diff --git a/CHANGES.txt b/CHANGES.txt index db1fa81e..3100c540 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +2.2.0 (March 28, 2025) + - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. + - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: + - `storage.expirationDays` to specify the validity period of the rollout cache. + - `storage.clearOnInit` to clear the rollout cache on SDK initialization. + - Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted. + 2.1.0 (January 17, 2025) - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs. diff --git a/package-lock.json b/package-lock.json index f15fce99..5a5d556b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -67,13 +67,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -266,18 +267,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -293,38 +294,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -495,9 +484,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -507,14 +496,14 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -542,14 +531,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1979,18 +1967,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2379,20 +2355,6 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -2459,21 +2421,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2812,15 +2759,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -3978,15 +3916,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -7465,18 +7394,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -7577,15 +7494,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8164,13 +8072,14 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -8319,15 +8228,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true }, "@babel/helper-validator-option": { @@ -8337,33 +8246,24 @@ "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" } }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" } }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -8482,23 +8382,23 @@ } }, "@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" } }, "@babel/traverse": { @@ -8520,14 +8420,13 @@ } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } }, "@bcoe/v8-coverage": { @@ -9608,15 +9507,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -9898,17 +9788,6 @@ "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -9956,21 +9835,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -10228,12 +10092,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -11089,12 +10947,6 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -13670,15 +13522,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -13757,12 +13600,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index d51bc14f..e7912d5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.2.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index 41176fbd..30c10631 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -2,7 +2,6 @@ import { parser } from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; -import { bucket } from '../../../utils/murmur3/murmur3'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on', async function () { @@ -662,8 +661,6 @@ test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async fu let evaluation = await evaluator(keyParser('aa'), 31, 100, 31); expect(evaluation.treatment).toBe('A'); // 20%:A // bucket 6 with murmur3 - console.log(bucket('b297', 31)); - evaluation = await evaluator(keyParser('b297'), 31, 100, 31); expect(evaluation.treatment).toBe('B'); // 20%:B // bucket 34 with murmur3 diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 520a5707..855675ff 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -20,9 +20,7 @@ export const RETRIEVE_CLIENT_EXISTING = 28; export const RETRIEVE_MANAGER = 29; export const SYNC_OFFLINE_DATA = 30; export const SYNC_SPLITS_FETCH = 31; -export const SYNC_SPLITS_NEW = 32; -export const SYNC_SPLITS_REMOVED = 33; -export const SYNC_SPLITS_SEGMENTS = 34; +export const SYNC_SPLITS_UPDATE = 32; export const STREAMING_NEW_MESSAGE = 35; export const SYNC_TASK_START = 36; export const SYNC_TASK_EXECUTE = 37; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index c89694e6..5dfcace3 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -21,9 +21,7 @@ export const codesDebug: [number, string][] = codesInfo.concat([ // synchronizer [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Feature flags data: \n%s'], [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up feature flags update using since = %s'], - [c.SYNC_SPLITS_NEW, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s'], - [c.SYNC_SPLITS_REMOVED, c.LOG_PREFIX_SYNC_SPLITS + 'Removed feature flags %s'], - [c.SYNC_SPLITS_SEGMENTS, c.LOG_PREFIX_SYNC_SPLITS + 'Segment names collected %s'], + [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s. Removed feature flags %s. Segment names collected %s'], [c.STREAMING_NEW_MESSAGE, c.LOG_PREFIX_SYNC_STREAMING + 'New SSE message received, with data: %s.'], [c.SYNC_TASK_START, c.LOG_PREFIX_SYNC + ': Starting %s. Running each %s millis'], [c.SYNC_TASK_EXECUTE, c.LOG_PREFIX_SYNC + ': Running %s'], diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 2c0b0c63..123f8eee 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -21,7 +21,7 @@ export const codesError: [number, string][] = [ // input validation [c.ERROR_EVENT_TYPE_FORMAT, '%s: you passed "%s", event_type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.'], [c.ERROR_NOT_PLAIN_OBJECT, '%s: %s must be a plain object.'], - [c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.'], + [c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded.'], [c.ERROR_NOT_FINITE, '%s: value must be a finite number.'], [c.ERROR_NULL, '%s: you passed a null or undefined %s. It must be a non-empty string.'], [c.ERROR_TOO_LONG, '%s: %s too long. It must have 250 characters or less.'], diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 52487f95..568771a8 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -18,7 +18,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], // input validation [c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'], - [c.WARN_TRIMMING_PROPERTIES, '%s: Event has more than 300 properties. Some of them will be trimmed when processed.'], + [c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'], [c.WARN_CONVERTING, '%s: %s "%s" is not of type string, converting.'], [c.WARN_TRIMMING, '%s: %s "%s" has extra whitespace, trimming.'], [c.WARN_NOT_EXISTENT_SPLIT, '%s: feature flag "%s" does not exist in this environment. Please double check what feature flags exist in the Split user interface.'], diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 9e2cf34a..174f1373 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -3,10 +3,14 @@ import { EventEmitter } from '../../utils/MinEvents'; import { IReadinessManager } from '../types'; import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants'; import { ISettings } from '../../types'; +import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; const settings = { startup: { readyTimeout: 0, + }, + storage: { + type: STORAGE_LOCALSTORAGE } } as unknown as ISettings; @@ -67,7 +71,14 @@ test('READINESS MANAGER / Ready event should be fired once', () => { const readinessManager = readinessManagerFactory(EventEmitter, settings); let counter = 0; + readinessManager.gate.on(SDK_READY_FROM_CACHE, () => { + expect(readinessManager.isReadyFromCache()).toBe(true); + expect(readinessManager.isReady()).toBe(true); + counter++; + }); + readinessManager.gate.on(SDK_READY, () => { + expect(readinessManager.isReadyFromCache()).toBe(true); expect(readinessManager.isReady()).toBe(true); counter++; }); @@ -79,7 +90,7 @@ test('READINESS MANAGER / Ready event should be fired once', () => { readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - expect(counter).toBe(1); // should be called once + expect(counter).toBe(2); // should be called once }); test('READINESS MANAGER / Ready from cache event should be fired once', (done) => { @@ -88,6 +99,7 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) = readinessManager.gate.on(SDK_READY_FROM_CACHE, () => { expect(readinessManager.isReadyFromCache()).toBe(true); + expect(readinessManager.isReady()).toBe(false); counter++; }); diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 6f46474d..c69eedce 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -3,6 +3,7 @@ import { ISettings } from '../types'; import SplitIO from '../../types/splitio'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants'; import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types'; +import { STORAGE_LOCALSTORAGE } from '../utils/constants'; function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter { const splitsEventEmitter = objectAssign(new EventEmitter(), { @@ -114,6 +115,10 @@ export function readinessManagerFactory( isReady = true; try { syncLastUpdate(); + if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) { + isReadyFromCache = true; + gate.emit(SDK_READY_FROM_CACHE); + } gate.emit(SDK_READY); } catch (e) { // throws user callback exceptions in next tick diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts index e007dc54..65aa8ef1 100644 --- a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -33,7 +33,7 @@ const clientMock = { } }; // @ts-expect-error -const client = clientAttributesDecoration(loggerMock, clientMock); +const client: any = clientAttributesDecoration(loggerMock, clientMock); test('ATTRIBUTES DECORATION / storage', () => { @@ -91,9 +91,7 @@ describe('ATTRIBUTES DECORATION / validation', () => { test('Should return false if it is an invalid attributes map', () => { expect(client.setAttribute('', 'attributeValue')).toEqual(false); // It should be invalid if the attribute key is not a string - // @ts-expect-error expect(client.setAttribute('attributeKey1', new Date())).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. - // @ts-expect-error expect(client.setAttribute('attributeKey2', { 'some': 'object' })).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(client.setAttribute('attributeKey3', Infinity)).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. @@ -153,180 +151,31 @@ describe('ATTRIBUTES DECORATION / validation', () => { describe('ATTRIBUTES DECORATION / evaluation', () => { - test('Evaluation attributes logic and precedence / getTreatment', () => { - + test.each([ + ['getTreatment', 'split'], + ['getTreatments', ['split']], + ['getTreatmentWithConfig', 'split'], + ['getTreatmentsWithConfig', ['split']], + ['getTreatmentsByFlagSet', 'set'], + ['getTreatmentsWithConfigByFlagSet', 'set'], + ['getTreatmentsByFlagSets', ['set']], + ['getTreatmentsWithConfigByFlagSets', ['set']] + ])('Evaluation attributes logic and precedence / %s', (method, param) => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatment('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client[method]('key', param)).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatment('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client[method]('key', param, null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client[method]('key', param, { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatment('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client[method]('key', param, null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client[method]('key', param)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatments', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatments('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatments('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatments('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentWithConfig', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentWithConfig('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentWithConfig('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfig', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsByFlagSets', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSets', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsByFlagSet', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSet', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - }); }); diff --git a/src/sdkClient/__tests__/clientInputValidation.spec.ts b/src/sdkClient/__tests__/clientInputValidation.spec.ts index 0664c179..f70845f7 100644 --- a/src/sdkClient/__tests__/clientInputValidation.spec.ts +++ b/src/sdkClient/__tests__/clientInputValidation.spec.ts @@ -3,13 +3,15 @@ import { clientInputValidationDecorator } from '../clientInputValidation'; // Mocks import { DebugLogger } from '../../logger/browser/DebugLogger'; +import { createClientMock } from './testUtils'; const settings: any = { log: DebugLogger(), sync: { __splitFiltersValidation: { groupedFilters: { bySet: [] } } } }; -const client: any = {}; +const EVALUATION_RESULT = 'on'; +const client: any = createClientMock(EVALUATION_RESULT); const readinessManager: any = { isReady: () => true, @@ -52,4 +54,54 @@ describe('clientInputValidationDecorator', () => { // @TODO should be 8, but there is an additional log from `getTreatmentsByFlagSet` and `getTreatmentsWithConfigByFlagSet` that should be removed expect(logSpy).toBeCalledTimes(10); }); + + test('should evaluate but log an error if the passed 4th argument (evaluation options) is invalid', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, 'invalid')).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatment: evaluation options must be a plain object.'); + expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatmentWithConfig('key', 'ff', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentWithConfig: properties must be a plain object.'); + expect(client.getTreatmentWithConfig).toBeCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatments('key', ['ff'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatments: properties must be a plain object.'); + expect(client.getTreatments).toBeCalledWith('key', ['ff'], undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfig('key', ['ff'], {}, { properties: true })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfig: properties must be a plain object.'); + expect(client.getTreatmentsWithConfig).toBeCalledWith('key', ['ff'], {}, undefined); + + expect(clientWithValidation.getTreatmentsByFlagSet('key', 'flagSet', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSet: properties must be a plain object.'); + expect(client.getTreatmentsByFlagSet).toBeCalledWith('key', 'flagset', undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfigByFlagSet('key', 'flagSet', {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSet: properties must be a plain object.'); + expect(client.getTreatmentsWithConfigByFlagSet).toBeCalledWith('key', 'flagset', {}, undefined); + + expect(clientWithValidation.getTreatmentsByFlagSets('key', ['flagSet'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSets: properties must be a plain object.'); + expect(client.getTreatmentsByFlagSets).toBeCalledWith('key', ['flagset'], undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfigByFlagSets('key', ['flagSet'], {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSets: properties must be a plain object.'); + expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith('key', ['flagset'], {}, undefined); + }); + + test('should sanitize the properties in the 4th argument', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: { toSanitize: /asd/, correct: 100 }})).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => getTreatment: Property "toSanitize" is of invalid type. Setting value to null.'); + expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, { properties: { toSanitize: null, correct: 100 }}); + }); + + test('should ignore the properties in the 4th argument if an empty object is passed', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: {} })).toBe(EVALUATION_RESULT); + expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: undefined })).toBe(EVALUATION_RESULT); + expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined); + + expect(logSpy).not.toBeCalled(); + }); }); diff --git a/src/sdkClient/__tests__/testUtils.ts b/src/sdkClient/__tests__/testUtils.ts index 901897e3..dab0085a 100644 --- a/src/sdkClient/__tests__/testUtils.ts +++ b/src/sdkClient/__tests__/testUtils.ts @@ -8,3 +8,18 @@ export function assertClientApi(client: any, sdkStatus?: object) { expect(typeof client[method]).toBe('function'); }); } + +export function createClientMock(returnValue: any) { + + return { + getTreatment: jest.fn(()=> returnValue), + getTreatmentWithConfig: jest.fn(()=> returnValue), + getTreatments: jest.fn(()=> returnValue), + getTreatmentsWithConfig: jest.fn(()=> returnValue), + getTreatmentsByFlagSets: jest.fn(()=> returnValue), + getTreatmentsWithConfigByFlagSets: jest.fn(()=> returnValue), + getTreatmentsByFlagSet: jest.fn(()=> returnValue), + getTreatmentsWithConfigByFlagSet: jest.fn(()=> returnValue), + track: jest.fn(()=> returnValue), + }; +} diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 1139a272..0e526f72 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -23,6 +23,14 @@ function treatmentsNotReady(featureFlagNames: string[]) { return evaluations; } +function stringify(options?: SplitIO.EvaluationOptions) { + if (options && options.properties) { + try { + return JSON.stringify(options.properties); + } catch { /* JSON.stringify should never throw with validated options, but handling just in case */ } + } +} + /** * Creator of base client with getTreatments and track methods. */ @@ -31,12 +39,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const { log, mode } = settings; const isAsync = isConsumerMode(mode); - function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined, withConfig = false, methodName = GET_TREATMENT) { + function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT); const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDecorated[] = []; - const treatment = processEvaluation(evaluationResult, featureFlagName, key, attributes, withConfig, methodName, queue); + const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue); impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); @@ -52,18 +60,19 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation); } - function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatment(key, featureFlagName, attributes, true, GET_TREATMENT_WITH_CONFIG); + function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG); } - function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, methodName = GET_TREATMENTS) { + function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS); const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; - const treatments: Record = {}; + const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {}; + const properties = stringify(options); Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue); }); impressionsTracker.track(queue, attributes); @@ -80,19 +89,19 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } - function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined) { - return getTreatments(key, featureFlagNames, attributes, true, GET_TREATMENTS_WITH_CONFIG); + function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG); } - function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { + function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { const stopTelemetryTracker = telemetryTracker.trackEval(method); const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; - const treatments: Record = {}; - const evaluations = evaluationResults; - Object.keys(evaluations).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); + const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {}; + const properties = stringify(options); + Object.keys(evaluationResults).forEach(featureFlagName => { + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue); }); impressionsTracker.track(queue, attributes); @@ -109,16 +118,16 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } - function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, flagSetNames, attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); + function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); } - function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [flagSetName], attributes, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET); + function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET); } - function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [flagSetName], attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET); + function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET); } // Internal function @@ -126,7 +135,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl evaluation: IEvaluationResult, featureFlagName: string, key: SplitIO.SplitKey, - attributes: SplitIO.Attributes | undefined, + properties: string | undefined, withConfig: boolean, invokingMethodName: string, queue: ImpressionDecorated[] @@ -148,6 +157,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl bucketingKey, label, changeNumber: changeNumber as number, + properties }, disabled: impressionsDisabled }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index cf31b5d3..b9a901db 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -20,50 +20,44 @@ export function clientAttributesDecoration 0) { - return objectAssign({}, storedAttributes, maybeAttributes); - } - return maybeAttributes; + return Object.keys(storedAttributes).length > 0 ? + objectAssign({}, storedAttributes, maybeAttributes) : + maybeAttributes; } return objectAssign(client, { @@ -75,7 +69,6 @@ export function clientAttributesDecoration -1 ? @@ -43,6 +44,7 @@ export function clientInputValidationDecorator res[split] = CONTROL); @@ -93,11 +96,11 @@ export function clientInputValidationDecorator res[split] = objectAssign({}, CONTROL_WITH_CONFIG)); @@ -106,41 +109,41 @@ export function clientInputValidationDecorator { const cache = new SplitsCacheInRedis(loggerMock, keys, connection); const manager = sdkManagerFactory({ mode: 'consumer', log: loggerMock }, cache, sdkReadinessManagerMock); await cache.clear(); - await cache.addSplit(splitObject.name, splitObject as any); + await cache.addSplit(splitObject as any); /** List all splits */ const views = await manager.splits(); diff --git a/src/sdkManager/__tests__/index.syncCache.spec.ts b/src/sdkManager/__tests__/index.syncCache.spec.ts index 3ca1cfa0..391a053c 100644 --- a/src/sdkManager/__tests__/index.syncCache.spec.ts +++ b/src/sdkManager/__tests__/index.syncCache.spec.ts @@ -19,7 +19,7 @@ describe('Manager with sync cache (In Memory)', () => { /** Setup: create manager */ const cache = new SplitsCacheInMemory(); const manager = sdkManagerFactory({ mode: 'standalone', log: loggerMock }, cache, sdkReadinessManagerMock); - cache.addSplit(splitObject.name, splitObject as any); + cache.addSplit(splitObject as any); test('List all splits', () => { diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index dcf059ed..420b9202 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -8,12 +8,22 @@ import { objectAssign } from '../utils/lang/objectAssign'; */ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { - abstract addSplit(name: string, split: ISplit): Promise - abstract addSplits(entries: [string, ISplit][]): Promise - abstract removeSplits(names: string[]): Promise + protected abstract addSplit(split: ISplit): Promise + protected abstract removeSplit(name: string): Promise + protected abstract setChangeNumber(changeNumber: number): Promise + + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(addedFF => this.addSplit(addedFF))), + Promise.all(toRemove.map(removedFF => this.removeSplit(removedFF.name))) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + abstract getSplit(name: string): Promise abstract getSplits(names: string[]): Promise> - abstract setChangeNumber(changeNumber: number): Promise abstract getChangeNumber(): Promise abstract getAll(): Promise abstract getSplitNames(): Promise @@ -27,14 +37,6 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { return Promise.resolve(true); } - /** - * Check if the splits information is already stored in cache. - * Noop, just keeping the interface. This is used by client-side implementations only. - */ - checkCache(): Promise { - return Promise.resolve(false); - } - /** * Kill `name` split and set `defaultTreatment` and `changeNumber`. * Used for SPLIT_KILL push notifications. @@ -52,7 +54,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { newSplit.defaultTreatment = defaultTreatment; newSplit.changeNumber = changeNumber; - return this.addSplit(name, newSplit); + return this.addSplit(newSplit); } return false; }).catch(() => false); diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index f82ebbd6..483deb42 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -9,16 +9,14 @@ import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; */ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { - abstract addSplit(name: string, split: ISplit): boolean - - addSplits(entries: [string, ISplit][]): boolean[] { - return entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1])); - } - - abstract removeSplit(name: string): boolean - - removeSplits(names: string[]): boolean[] { - return names.map(name => this.removeSplit(name)); + protected abstract addSplit(split: ISplit): boolean + protected abstract removeSplit(name: string): boolean + protected abstract setChangeNumber(changeNumber: number): boolean | void + + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean { + this.setChangeNumber(changeNumber); + const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result); + return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; } abstract getSplit(name: string): ISplit | null @@ -31,8 +29,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { return splits; } - abstract setChangeNumber(changeNumber: number): boolean | void - abstract getChangeNumber(): number getAll(): ISplit[] { @@ -47,14 +43,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { abstract clear(): void - /** - * Check if the splits information is already stored in cache. This data can be preloaded. - * It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE. - */ - checkCache(): boolean { - return false; - } - /** * Kill `name` split and set `defaultTreatment` and `changeNumber`. * Used for SPLIT_KILL push notifications. @@ -71,7 +59,7 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { newSplit.defaultTreatment = defaultTreatment; newSplit.changeNumber = changeNumber; - return this.addSplit(name, newSplit); + return this.addSplit(newSplit); } return false; } diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index e70b251b..2f5dc800 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -1,5 +1,4 @@ import { ISettings } from '../types'; -import { startsWith } from '../utils/lang'; import { hash } from '../utils/murmur3/murmur3'; const everythingAtTheEnd = /[^.]+$/; @@ -34,24 +33,10 @@ export class KeyBuilder { return `${this.prefix}.splits.till`; } - // NOT USED - // buildSplitsReady() { - // return `${this.prefix}.splits.ready`; - // } - - isSplitKey(key: string) { - return startsWith(key, `${this.prefix}.split.`); - } - buildSplitKeyPrefix() { return `${this.prefix}.split.`; } - // Only used by InLocalStorage. - buildSplitsWithSegmentCountKey() { - return `${this.prefix}.splits.usingSegments`; - } - buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } @@ -60,11 +45,6 @@ export class KeyBuilder { return `${this.prefix}.segment.${segmentName}.till`; } - // NOT USED - // buildSegmentsReady() { - // return `${this.prefix}.segments.ready`; - // } - extractKey(builtKey: string) { const s = builtKey.match(everythingAtTheEnd); diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index a59d7208..d3404ed1 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -28,8 +28,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { extractSegmentName(builtSegmentKeyName: string) { const prefix = `${this.prefix}.${this.matchingKey}.segment.`; - if (startsWith(builtSegmentKeyName, prefix)) - return builtSegmentKeyName.substr(prefix.length); + if (startsWith(builtSegmentKeyName, prefix)) return builtSegmentKeyName.slice(prefix.length); } buildLastUpdatedKey() { @@ -43,6 +42,18 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { buildTillKey() { return `${this.prefix}.${this.matchingKey}.segments.till`; } + + isSplitKey(key: string) { + return startsWith(key, `${this.prefix}.split.`); + } + + buildSplitsWithSegmentCountKey() { + return `${this.prefix}.splits.usingSegments`; + } + + buildLastClear() { + return `${this.prefix}.lastClear`; + } } export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder { @@ -54,7 +65,7 @@ export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): extractSegmentName(builtSegmentKeyName: string) { const p = `${prefix}.${matchingKey}.largeSegment.`; - if (startsWith(builtSegmentKeyName, p)) return builtSegmentKeyName.substr(p.length); + if (startsWith(builtSegmentKeyName, p)) return builtSegmentKeyName.slice(p.length); }, buildTillKey() { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index e0494ec9..45af194c 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -9,14 +9,9 @@ test('KEYS / splits keys', () => { const expectedKey = `SPLITIO.split.${splitName}`; const expectedTill = 'SPLITIO.splits.till'; - expect(builder.isSplitKey(expectedKey)).toBe(true); - expect(builder.buildSplitKey(splitName) === expectedKey).toBe(true); - expect(builder.buildSplitsTillKey() === expectedTill).toBe(true); - expect(builder.extractKey(builder.buildSplitKey(splitName)) === splitName).toBe(true); - - // NOT USED - // const expectedReady = 'SPLITIO.splits.ready'; - // expect(builder.buildSplitsReady() === expectedReady).toBe(true); + expect(builder.buildSplitKey(splitName)).toBe(expectedKey); + expect(builder.buildSplitsTillKey()).toBe(expectedTill); + expect(builder.extractKey(builder.buildSplitKey(splitName))).toBe(splitName); }); test('KEYS / splits keys with custom prefix', () => { @@ -27,13 +22,8 @@ test('KEYS / splits keys with custom prefix', () => { const expectedKey = `${prefix}.split.${splitName}`; const expectedTill = `${prefix}.splits.till`; - expect(builder.isSplitKey(expectedKey)).toBe(true); expect(builder.buildSplitKey(splitName)).toBe(expectedKey); expect(builder.buildSplitsTillKey() === expectedTill).toBe(true); - - // NOT USED - // const expectedReady = `${prefix}.SPLITIO.splits.ready`; - // expect(builder.buildSplitsReady() === expectedReady).toBe(true); }); const prefix = 'SPLITIO'; diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index 94e11c36..fa38944f 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -23,9 +23,9 @@ export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpr // Split mocks //@ts-ignore -export const splitWithUserTT: ISplit = { trafficTypeName: 'user_tt', conditions: [] }; +export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; //@ts-ignore -export const splitWithAccountTT: ISplit = { trafficTypeName: 'account_tt', conditions: [] }; +export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; //@ts-ignore export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; //@ts-ignore diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index ce288868..49522bce 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -1,7 +1,9 @@ import { PreloadedData } from '../types'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser'; import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types'; +// This value might be eventually set via a config parameter +const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days + /** * Factory of client-side storage loader * @@ -35,10 +37,9 @@ export function dataLoaderFactory(preloadedData: PreloadedData): DataLoader { // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data storage.splits.clear(); - storage.splits.setChangeNumber(since); // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data - storage.splits.addSplits(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName]))); + storage.splits.update(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName])), [], since); // add mySegments data let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 93eb6f32..c3cb3142 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -5,7 +5,6 @@ import { KeyBuilderCS } from '../KeyBuilderCS'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISettings } from '../../types'; -import { getStorageHash } from '../KeyBuilder'; import { setToArray } from '../../utils/lang/sets'; /** @@ -15,21 +14,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly log: ILogger; - private readonly storageHash: string; private readonly flagSetsFilter: string[]; private hasSync?: boolean; - private updateNewFilter?: boolean; - constructor(settings: ISettings, keys: KeyBuilderCS, expirationTimestamp?: number) { + constructor(settings: ISettings, keys: KeyBuilderCS) { super(); this.keys = keys; this.log = settings.log; - this.storageHash = getStorageHash(settings); this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet; - - this._checkExpiration(expirationTimestamp); - - this._checkFilterQuery(); } private _decrementCount(key: string) { @@ -39,16 +31,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { else localStorage.removeItem(key); } - private _decrementCounts(split: ISplit | null) { + private _decrementCounts(split: ISplit) { try { - if (split) { - const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - this._decrementCount(ttKey); + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + this._decrementCount(ttKey); - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - this._decrementCount(segmentsCountKey); - } + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + this._decrementCount(segmentsCountKey); } } catch (e) { this.log.error(LOG_PREFIX + e); @@ -79,8 +69,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage. */ clear() { - this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage'); - // collect item keys const len = localStorage.length; const accum = []; @@ -96,18 +84,21 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.hasSync = false; } - addSplit(name: string, split: ISplit) { + addSplit(split: ISplit) { try { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); const splitFromLocalStorage = localStorage.getItem(splitKey); const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null; + if (previousSplit) { + this._decrementCounts(previousSplit); + this.removeFromFlagSets(previousSplit.name, previousSplit.sets); + } + localStorage.setItem(splitKey, JSON.stringify(split)); this._incrementCounts(split); - this._decrementCounts(previousSplit); - - if (previousSplit) this.removeFromFlagSets(previousSplit.name, previousSplit.sets); this.addToFlagSets(split); return true; @@ -120,10 +111,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { removeSplit(name: string): boolean { try { const split = this.getSplit(name); + if (!split) return false; + localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); - if (split) this.removeFromFlagSets(split.name, split.sets); + this.removeFromFlagSets(split.name, split.sets); return true; } catch (e) { @@ -132,25 +125,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - getSplit(name: string) { + getSplit(name: string): ISplit | null { const item = localStorage.getItem(this.keys.buildSplitKey(name)); return item && JSON.parse(item); } setChangeNumber(changeNumber: number): boolean { - - // when using a new split query, we must update it at the store - if (this.updateNewFilter) { - this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); - const storageHashKey = this.keys.buildHashKey(); - try { - localStorage.setItem(storageHashKey, this.storageHash); - } catch (e) { - this.log.error(LOG_PREFIX + e); - } - this.updateNewFilter = false; - } - try { localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); // update "last updated" timestamp with current time @@ -212,48 +192,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - /** - * Check if the splits information is already stored in browser LocalStorage. - * In this function we could add more code to check if the data is valid. - * @override - */ - checkCache(): boolean { - return this.getChangeNumber() > -1; - } - - /** - * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`, - * - * @param expirationTimestamp - if the value is not a number, data will not be cleaned - */ - private _checkExpiration(expirationTimestamp?: number) { - let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear(); - } - } - - // @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage - private _checkFilterQuery() { - const storageHashKey = this.keys.buildHashKey(); - const storageHash = localStorage.getItem(storageHashKey); - - if (storageHash !== this.storageHash) { - try { - // mark cache to update the new query filter on first successful splits fetch - this.updateNewFilter = true; - - // if there is cache, clear it - if (this.checkCache()) this.clear(); - - } catch (e) { - this.log.error(LOG_PREFIX + e); - } - } - // if the filter didn't change, nothing is done - } - getNamesByFlagSets(flagSets: string[]): Set[] { return flagSets.map(flagSet => { const flagSetKey = this.keys.buildFlagSetKey(flagSet); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 4d8ec076..913d6a3b 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -5,80 +5,68 @@ import { ISplit } from '../../../dtos/types'; import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; -test('SPLIT CACHE / LocalStorage', () => { +test('SPLITS CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); cache.clear(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], -1); let values = cache.getAll(); expect(values).toEqual([something, somethingElse]); - cache.removeSplit('lol1'); + cache.removeSplit(something.name); - const splits = cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1']).toEqual(null); - expect(splits['lol2']).toEqual(somethingElse); + const splits = cache.getSplits([something.name, somethingElse.name]); + expect(splits[something.name]).toEqual(null); + expect(splits[somethingElse.name]).toEqual(somethingElse); values = cache.getAll(); expect(values).toEqual([somethingElse]); - expect(cache.getSplit('lol1')).toEqual(null); - expect(cache.getSplit('lol2')).toEqual(somethingElse); + expect(cache.getSplit(something.name)).toEqual(null); + expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); - expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. - - expect(cache.getChangeNumber() === -1).toBe(true); - - expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. + expect(cache.getChangeNumber()).toBe(-1); cache.setChangeNumber(123); - expect(cache.checkCache()).toBe(true); // checkCache should return true once localstorage has data. - - expect(cache.getChangeNumber() === 123).toBe(true); - + expect(cache.getChangeNumber()).toBe(123); }); -test('SPLIT CACHE / LocalStorage / Get Keys', () => { +test('SPLITS CACHE / LocalStorage / Get Keys', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], 1); const keys = cache.getSplitNames(); - expect(keys.indexOf('lol1') !== -1).toBe(true); - expect(keys.indexOf('lol2') !== -1).toBe(true); + expect(keys.indexOf(something.name) !== -1).toBe(true); + expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); }); -test('SPLIT CACHE / LocalStorage / Add Splits', () => { +test('SPLITS CACHE / LocalStorage / Update Splits', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplits([ - ['lol1', something], - ['lol2', somethingElse] - ]); + cache.update([something, somethingElse], [], 1); - cache.removeSplits(['lol1', 'lol2']); + cache.update([], [something, somethingElse], 1); - expect(cache.getSplit('lol1') == null).toBe(true); - expect(cache.getSplit('lol2') == null).toBe(true); + expect(cache.getSplit(something.name)).toBe(null); + expect(cache.getSplit(somethingElse.name)).toBe(null); }); -test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { +test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplits([ // loop of addSplit - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - cache.addSplit('split4', splitWithUserTT); + cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], 1); + cache.addSplit({ ...splitWithUserTT, name: 'split4' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); @@ -89,7 +77,8 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); - cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + cache.removeSplit('split3'); + cache.removeSplit('split2'); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(false); @@ -99,19 +88,20 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(false); expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit('split1', splitWithUserTT); + cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit('split1', splitWithAccountTT); + cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(cache.trafficTypeExists('account_tt')).toBe(true); expect(cache.trafficTypeExists('user_tt')).toBe(false); }); -test('SPLIT CACHE / LocalStorage / killLocally', () => { +test('SPLITS CACHE / LocalStorage / killLocally', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + + cache.addSplit(something); + cache.addSplit(somethingElse); const initialChangeNumber = cache.getChangeNumber(); // kill an non-existent split @@ -122,8 +112,8 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment', 100); + let lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -132,27 +122,27 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment_2', 90); + lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older }); -test('SPLIT CACHE / LocalStorage / usesSegments', () => { +test('SPLITS CACHE / LocalStorage / usesSegments', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. - cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); + cache.update([splitWithUserTT, splitWithAccountTT], [], 1); expect(cache.usesSegments()).toBe(false); // 0 splits using segments - cache.addSplit('split3', splitWithAccountTTAndUsesSegments); + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split3' }); expect(cache.usesSegments()).toBe(true); // 1 split using segments - cache.addSplit('split4', splitWithAccountTTAndUsesSegments); + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split4' }); expect(cache.usesSegments()).toBe(true); // 2 splits using segments cache.removeSplit('split3'); @@ -162,7 +152,7 @@ test('SPLIT CACHE / LocalStorage / usesSegments', () => { expect(cache.usesSegments()).toBe(false); // 0 splits using segments }); -test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { +test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { // @ts-ignore const cache = new SplitsCacheInLocal({ ...fullSettings, @@ -173,14 +163,15 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { } } }, new KeyBuilderCS('SPLITIO', 'user')); + const emptySet = new Set([]); - cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -188,13 +179,13 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -206,31 +197,31 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(cache.getNamesByFlagSets([])).toEqual([]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); }); // if FlagSets are not defined, it should store all FlagSets in memory. test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { - const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const emptySet = new Set([]); - cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); - expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual([emptySet]); - expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); + expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); + expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); + expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); // Validate that the feature flag cache is cleared when calling `clear` method - cacheWithoutFilters.clear(); - expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage - expect(localStorage.key(0)).toBe('SPLITIO.hash'); + cache.clear(); + expect(localStorage.length).toBe(0); }); diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts new file mode 100644 index 00000000..27050a56 --- /dev/null +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -0,0 +1,125 @@ +import { validateCache } from '../validateCache'; + +import { KeyBuilderCS } from '../../KeyBuilderCS'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; +import { nearlyEqual } from '../../../__tests__/testUtils'; +import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; + +const FULL_SETTINGS_HASH = '404832b3'; + +describe('validateCache', () => { + const keys = new KeyBuilderCS('SPLITIO', 'user'); + const logSpy = jest.spyOn(fullSettings.log, 'info'); + const segments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const splits = new SplitsCacheInLocal(fullSettings, keys); + + jest.spyOn(splits, 'clear'); + jest.spyOn(splits, 'getChangeNumber'); + jest.spyOn(segments, 'clear'); + jest.spyOn(largeSegments, 'clear'); + + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + }); + + test('if there is no cache, it should return false', () => { + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it must not be cleared, it should return true', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it has expired, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago + + expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and its hash has changed, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe('aa4877c2'); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and clearOnInit is true, it should clear cache and return false', () => { + // Older cache version (without last clear) + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + const lastClear = localStorage.getItem(keys.buildLastClear()); + expect(nearlyEqual(parseInt(lastClear as string), Date.now())).toBe(true); + + // If cache is cleared, it should not clear again until a day has passed + logSpy.mockClear(); + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(logSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed + + // If a day has passed, it should clear again + localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + expect(splits.clear).toHaveBeenCalledTimes(2); + expect(segments.clear).toHaveBeenCalledTimes(2); + expect(largeSegments.clear).toHaveBeenCalledTimes(2); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); +}); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index c621141d..616bb7d7 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -7,22 +7,19 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import { SplitsCacheInLocal } from './SplitsCacheInLocal'; import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; - -export interface InLocalStorageOptions { - prefix?: string -} +import { validateCache } from './validateCache'; +import SplitIO from '../../../types/splitio'; /** * InLocal storage factory for standalone client-side SplitFactory */ -export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyncFactory { +export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): IStorageSyncFactory { const prefix = validatePrefix(options.prefix); @@ -37,9 +34,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params; const matchingKey = getMatching(settings.core.key); const keys = new KeyBuilderCS(prefix, matchingKey); - const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; - const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp); + const splits = new SplitsCacheInLocal(settings, keys); const segments = new MySegmentsCacheInLocal(log, keys); const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)); @@ -53,6 +49,10 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: new UniqueKeysCacheInMemoryCS(), + validateCache() { + return validateCache(options, settings, keys, splits, segments, largeSegments); + }, + destroy() { }, // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key). diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts new file mode 100644 index 00000000..c9bd78d2 --- /dev/null +++ b/src/storages/inLocalStorage/validateCache.ts @@ -0,0 +1,91 @@ +import { ISettings } from '../../types'; +import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; +import { getStorageHash } from '../KeyBuilder'; +import { LOG_PREFIX } from './constants'; +import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; +import { KeyBuilderCS } from '../KeyBuilderCS'; +import SplitIO from '../../../types/splitio'; + +const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; +const MILLIS_IN_A_DAY = 86400000; + +/** + * Validates if cache should be cleared and sets the cache `hash` if needed. + * + * @returns `true` if cache should be cleared, `false` otherwise + */ +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) { + const { log } = settings; + + // Check expiration + const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10); + if (!isNaNNumber(lastUpdatedTimestamp)) { + const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; + const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; + if (lastUpdatedTimestamp < expirationTimestamp) { + log.info(LOG_PREFIX + 'Cache expired more than ' + cacheExpirationInDays + ' days ago. Cleaning up cache'); + return true; + } + } + + // Check hash + const storageHashKey = keys.buildHashKey(); + const storageHash = localStorage.getItem(storageHashKey); + const currentStorageHash = getStorageHash(settings); + + if (storageHash !== currentStorageHash) { + try { + localStorage.setItem(storageHashKey, currentStorageHash); + } catch (e) { + log.error(LOG_PREFIX + e); + } + if (isThereCache) { + log.info(LOG_PREFIX + 'SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); + return true; + } + return false; // No cache to clear + } + + // Clear on init + if (options.clearOnInit) { + const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10); + + if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) { + log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + return true; + } + } +} + +/** + * Clean cache if: + * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` + * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + * - `clearOnInit` was set and cache was not cleared in the last 24 hours + * + * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) + */ +export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { + + const currentTimestamp = Date.now(); + const isThereCache = splits.getChangeNumber() > -1; + + if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { + splits.clear(); + segments.clear(); + largeSegments.clear(); + + // Update last clear timestamp + try { + localStorage.setItem(keys.buildLastClear(), currentTimestamp + ''); + } catch (e) { + settings.log.error(LOG_PREFIX + e); + } + + return false; + } + + // Check if ready from cache + return isThereCache; +} diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 688b6e24..a8be688a 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -26,7 +26,8 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { this.segmentsCount = 0; } - addSplit(name: string, split: ISplit): boolean { + addSplit(split: ISplit): boolean { + const name = split.name; const previousSplit = this.getSplit(name); if (previousSplit) { // We had this Split already @@ -40,41 +41,35 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { if (usesSegments(previousSplit)) this.segmentsCount--; } - if (split) { - // Store the Split. - this.splitsCache[name] = split; - // Update TT cache - const ttName = split.trafficTypeName; - this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; - this.addToFlagSets(split); + // Store the Split. + this.splitsCache[name] = split; + // Update TT cache + const ttName = split.trafficTypeName; + this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; + this.addToFlagSets(split); - // Add to segments count for the new version of the Split - if (usesSegments(split)) this.segmentsCount++; + // Add to segments count for the new version of the Split + if (usesSegments(split)) this.segmentsCount++; - return true; - } else { - return false; - } + return true; } removeSplit(name: string): boolean { const split = this.getSplit(name); - if (split) { - // Delete the Split - delete this.splitsCache[name]; + if (!split) return false; - const ttName = split.trafficTypeName; - this.ttCache[ttName]--; // Update tt cache - if (!this.ttCache[ttName]) delete this.ttCache[ttName]; - this.removeFromFlagSets(split.name, split.sets); + // Delete the Split + delete this.splitsCache[name]; - // Update the segments count. - if (usesSegments(split)) this.segmentsCount--; + const ttName = split.trafficTypeName; + this.ttCache[ttName]--; // Update tt cache + if (!this.ttCache[ttName]) delete this.ttCache[ttName]; + this.removeFromFlagSets(split.name, split.sets); - return true; - } else { - return false; - } + // Update the segments count. + if (usesSegments(split)) this.segmentsCount--; + + return true; } getSplit(name: string): ISplit | null { diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 62812586..2f907eca 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -5,54 +5,62 @@ import { splitWithUserTT, splitWithAccountTT, something, somethingElse, featureF test('SPLITS CACHE / In Memory', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], -1); let values = cache.getAll(); - expect(values.indexOf(something) !== -1).toBe(true); - expect(values.indexOf(somethingElse) !== -1).toBe(true); + expect(values).toEqual([something, somethingElse]); - cache.removeSplit('lol1'); + cache.removeSplit(something.name); - const splits = cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1'] === null).toBe(true); - expect(splits['lol2'] === somethingElse).toBe(true); + const splits = cache.getSplits([something.name, somethingElse.name]); + expect(splits[something.name]).toEqual(null); + expect(splits[somethingElse.name]).toEqual(somethingElse); values = cache.getAll(); - expect(values.indexOf(something) === -1).toBe(true); - expect(values.indexOf(somethingElse) !== -1).toBe(true); + expect(values).toEqual([somethingElse]); - expect(cache.getSplit('lol1') == null).toBe(true); - expect(cache.getSplit('lol2') === somethingElse).toBe(true); + expect(cache.getSplit(something.name)).toEqual(null); + expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); + expect(cache.getChangeNumber()).toBe(-1); cache.setChangeNumber(123); - expect(cache.getChangeNumber() === 123).toBe(true); + expect(cache.getChangeNumber()).toBe(123); }); test('SPLITS CACHE / In Memory / Get Keys', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], 1); - let keys = cache.getSplitNames(); + const keys = cache.getSplitNames(); - expect(keys.indexOf('lol1') !== -1).toBe(true); - expect(keys.indexOf('lol2') !== -1).toBe(true); + expect(keys.indexOf(something.name) !== -1).toBe(true); + expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); +}); + +test('SPLITS CACHE / In Memory / Update Splits', () => { + const cache = new SplitsCacheInMemory(); + + cache.update([something, somethingElse], [], 1); + + cache.update([], [something, somethingElse], 1); + + expect(cache.getSplit(something.name)).toBe(null); + expect(cache.getSplit(somethingElse.name)).toBe(null); }); test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInMemory(); - cache.addSplits([ // loop of addSplit - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - cache.addSplit('split4', splitWithUserTT); + cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], 1); + cache.addSplit({ ...splitWithUserTT, name: 'split4' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); @@ -63,7 +71,8 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); - cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + cache.removeSplit('split3'); + cache.removeSplit('split2'); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(false); @@ -73,10 +82,10 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(false); expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit('split1', splitWithUserTT); + cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit('split1', splitWithAccountTT); + cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(cache.trafficTypeExists('account_tt')).toBe(true); expect(cache.trafficTypeExists('user_tt')).toBe(false); @@ -84,30 +93,30 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { test('SPLITS CACHE / In Memory / killLocally', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.addSplit(something); + cache.addSplit(somethingElse); const initialChangeNumber = cache.getChangeNumber(); // kill an non-existent split let updated = cache.killLocally('nonexistent_split', 'other_treatment', 101); const nonexistentSplit = cache.getSplit('nonexistent_split'); - expect(updated).toBe(false); // killLocally resolves without update if split doesn\'t exist + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment', 100); + let lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed - expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have the given default treatment + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment_2', 90); + lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older @@ -119,12 +128,12 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { const cache = new SplitsCacheInMemory({ groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new Set([]); - cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -132,13 +141,13 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -150,21 +159,21 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(cache.getNamesByFlagSets([])).toEqual([]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); }); // if FlagSets are not defined, it should store all FlagSets in memory. -test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { +test('SPLITS CACHE / In Memory / flag set cache tests without filters', () => { const cacheWithoutFilters = new SplitsCacheInMemory(); const emptySet = new Set([]); - cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 7ae68e64..ec360b2f 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -82,7 +82,8 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * The returned promise is resolved when the operation success * or rejected if it fails (e.g., redis operation fails) */ - addSplit(name: string, split: ISplit): Promise { + addSplit(split: ISplit): Promise { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); return this.redis.get(splitKey).then(splitFromStorage => { @@ -107,18 +108,9 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { }).then(() => true); } - /** - * Add a list of splits. - * The returned promise is resolved when the operation success - * or rejected if it fails (e.g., redis operation fails) - */ - addSplits(entries: [string, ISplit][]): Promise { - return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); - } - /** * Remove a given split. - * The returned promise is resolved when the operation success, with 1 or 0 indicating if the split existed or not. + * The returned promise is resolved when the operation success, with true or false indicating if the split existed (and was removed) or not. * or rejected if it fails (e.g., redis operation fails). */ removeSplit(name: string) { @@ -127,19 +119,10 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { return this._decrementCounts(split).then(() => this._updateFlagSets(name, split.sets)); } }).then(() => { - return this.redis.del(this.keys.buildSplitKey(name)); + return this.redis.del(this.keys.buildSplitKey(name)).then(status => status === 1); }); } - /** - * Remove a list of splits. - * The returned promise is resolved when the operation success, - * or rejected if it fails (e.g., redis operation fails). - */ - removeSplits(names: string[]): Promise { - return Promise.all(names.map(name => this.removeSplit(name))); - } - /** * Get split definition or null if it's not defined. * Returned promise is rejected if redis operation fails. diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 3f577254..0cbc8914 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -11,14 +11,11 @@ const keysBuilder = new KeyBuilderSS(prefix, metadata); describe('SPLITS CACHE REDIS', () => { - test('add/remove/get splits & set/get change number', async () => { + test('add/remove/get splits', async () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplits([ - ['lol1', splitWithUserTT], - ['lol2', splitWithAccountTT] - ]); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); let values = await cache.getAll(); @@ -27,33 +24,34 @@ describe('SPLITS CACHE REDIS', () => { let splitNames = await cache.getSplitNames(); - expect(splitNames.indexOf('lol1') !== -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.length).toBe(2); + expect(splitNames.indexOf('user_ff') !== -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - await cache.removeSplit('lol1'); + await cache.removeSplit('user_ff'); values = await cache.getAll(); expect(values).toEqual([splitWithAccountTT]); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); + expect(await cache.getSplit('user_ff')).toEqual(null); + expect(await cache.getSplit('account_ff')).toEqual(splitWithAccountTT); await cache.setChangeNumber(123); - expect(await cache.getChangeNumber() === 123).toBe(true); + expect(await cache.getChangeNumber()).toBe(123); splitNames = await cache.getSplitNames(); - expect(splitNames.indexOf('lol1') === -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.indexOf('user_ff') === -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - const splits = await cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1']).toEqual(null); - expect(splits['lol2']).toEqual(splitWithAccountTT); + const splits = await cache.getSplits(['user_ff', 'account_ff']); + expect(splits['user_ff']).toEqual(null); + expect(splits['account_ff']).toEqual(splitWithAccountTT); // Teardown. @TODO use cache clear method when implemented await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); - await connection.del(keysBuilder.buildSplitKey('lol2')); + await connection.del(keysBuilder.buildSplitKey('account_ff')); await connection.del(keysBuilder.buildSplitsTillKey()); await connection.disconnect(); }); @@ -62,13 +60,13 @@ describe('SPLITS CACHE REDIS', () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplits([ - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - await cache.addSplit('split4', splitWithUserTT); - await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + await cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], -1); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); // trying to add the same definition for an already added split will not have effect expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(true); @@ -81,7 +79,8 @@ describe('SPLITS CACHE REDIS', () => { expect(await connection.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); - await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + await cache.removeSplit('split3'); + await cache.removeSplit('split2'); expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(false); @@ -93,10 +92,10 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.trafficTypeExists('user_tt')).toBe(false); expect(await cache.trafficTypeExists('account_tt')).toBe(false); - await cache.addSplit('split1', splitWithUserTT); + await cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(await cache.trafficTypeExists('user_tt')).toBe(true); - await cache.addSplit('split1', splitWithAccountTT); + await cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(await cache.trafficTypeExists('account_tt')).toBe(true); expect(await cache.trafficTypeExists('user_tt')).toBe(false); @@ -111,8 +110,7 @@ describe('SPLITS CACHE REDIS', () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplit('lol1', splitWithUserTT); - await cache.addSplit('lol2', splitWithAccountTT); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); const initialChangeNumber = await cache.getChangeNumber(); // kill an non-existent split @@ -123,8 +121,8 @@ describe('SPLITS CACHE REDIS', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = await cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment', 100); + let lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -133,14 +131,15 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = await cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment_2', 90); + lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.removeSplits(['lol1', 'lol2']); + await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); @@ -151,12 +150,12 @@ describe('SPLITS CACHE REDIS', () => { const emptySet = new Set([]); - await cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + await cache.addSplit(featureFlagWithEmptyFS); expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -164,13 +163,13 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(await cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(await cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(await cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -188,11 +187,12 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(await cache.getNamesByFlagSets([])).toEqual([]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + await cache.addSplit({ ...featureFlagWithoutFS, name: featureFlagWithEmptyFS.name }); expect(await cache.getNamesByFlagSets([])).toEqual([]); // Delete splits, TT and flag set keys - await cache.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagWithEmptyFS.name]); + await cache.update([], [featureFlagThree, featureFlagTwo, featureFlagWithEmptyFS], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); @@ -204,12 +204,12 @@ describe('SPLITS CACHE REDIS', () => { const emptySet = new Set([]); - await cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree + ], [], -1); + await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(await cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -219,7 +219,8 @@ describe('SPLITS CACHE REDIS', () => { expect(await cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); // Delete splits, TT and flag set keys - await cacheWithoutFilters.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagOne.name, featureFlagWithEmptyFS.name]); + await cacheWithoutFilters.update([], [featureFlagThree, featureFlagTwo, featureFlagOne, featureFlagWithEmptyFS], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index ddb06149..9b53f3a9 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -66,7 +66,8 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { * The returned promise is resolved when the operation success * or rejected if it fails (e.g., wrapper operation fails) */ - addSplit(name: string, split: ISplit): Promise { + addSplit(split: ISplit): Promise { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); return this.wrapper.get(splitKey).then(splitFromStorage => { @@ -91,15 +92,6 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { }).then(() => true); } - /** - * Add a list of splits. - * The returned promise is resolved when the operation success - * or rejected if it fails (e.g., wrapper operation fails) - */ - addSplits(entries: [string, ISplit][]): Promise { - return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); - } - /** * Remove a given split. * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. @@ -115,15 +107,6 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { }); } - /** - * Remove a list of splits. - * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. - * or rejected if it fails (e.g., wrapper operation fails). - */ - removeSplits(names: string[]): Promise { // @ts-ignore - return Promise.all(names.map(name => this.removeSplit(name))); - } - /** * Get split. * The returned promise is resolved with the split definition or null if it's not defined, diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index 57fc34b3..03d1ee6e 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -12,74 +12,59 @@ describe('SPLITS CACHE PLUGGABLE', () => { test('add/remove/get splits', async () => { const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); - // Assert addSplit and addSplits - await cache.addSplits([ - ['lol1', splitWithUserTT], - ['lol2', splitWithAccountTT] - ]); - await cache.addSplit('lol3', splitWithAccountTT); - - // Assert getAll + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); + let values = await cache.getAll(); - expect(values).toEqual([splitWithUserTT, splitWithAccountTT, splitWithAccountTT]); + expect(values).toEqual([splitWithUserTT, splitWithAccountTT]); // Assert getSplits - let valuesObj = await cache.getSplits(['lol2', 'lol3']); - - expect(Object.keys(valuesObj).length).toBe(2); - expect(valuesObj.lol2).toEqual(splitWithAccountTT); - expect(valuesObj.lol3).toEqual(splitWithAccountTT); + let valuesObj = await cache.getSplits([splitWithUserTT.name, splitWithAccountTT.name]); + expect(valuesObj).toEqual(values.reduce>((acc, split) => { + acc[split.name] = split; + return acc; + }, {})); // Assert getSplitNames let splitNames = await cache.getSplitNames(); - expect(splitNames.length).toBe(3); - expect(splitNames.indexOf('lol1') !== -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); - expect(splitNames.indexOf('lol3') !== -1).toBe(true); - - // Assert removeSplit - await cache.removeSplit('lol1'); + expect(splitNames.length).toBe(2); + expect(splitNames.indexOf('user_ff') !== -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - values = await cache.getAll(); - expect(values.length).toBe(2); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); - - // Assert removeSplits - await cache.addSplit('lol1', splitWithUserTT); - await cache.removeSplits(['lol1', 'lol3']); + await cache.removeSplit('user_ff'); values = await cache.getAll(); - expect(values.length).toBe(1); - splitNames = await cache.getSplitNames(); - expect(splitNames.length).toBe(1); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); - }); + expect(values).toEqual([splitWithAccountTT]); - test('set/get change number', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); + expect(await cache.getSplit('user_ff')).toEqual(null); + expect(await cache.getSplit('account_ff')).toEqual(splitWithAccountTT); - expect(await cache.getChangeNumber()).toBe(-1); // if not set yet, changeNumber is -1 await cache.setChangeNumber(123); expect(await cache.getChangeNumber()).toBe(123); + splitNames = await cache.getSplitNames(); + + expect(splitNames.indexOf('user_ff') === -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); + + const splits = await cache.getSplits(['user_ff', 'account_ff']); + expect(splits['user_ff']).toEqual(null); + expect(splits['account_ff']).toEqual(splitWithAccountTT); }); test('trafficTypeExists', async () => { const wrapper = wrapperMockFactory(); const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper); - await cache.addSplits([ - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - await cache.addSplit('split4', splitWithUserTT); - await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + await cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], -1); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); // trying to add the same definition for an already added split will not have effect expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(true); @@ -92,7 +77,8 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await wrapper.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); - await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + await cache.removeSplit('split3'); + await cache.removeSplit('split2'); expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(false); @@ -104,21 +90,19 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.trafficTypeExists('user_tt')).toBe(false); expect(await cache.trafficTypeExists('account_tt')).toBe(false); - await cache.addSplit('split1', splitWithUserTT); + await cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(await cache.trafficTypeExists('user_tt')).toBe(true); - await cache.addSplit('split1', splitWithAccountTT); + await cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(await cache.trafficTypeExists('account_tt')).toBe(true); expect(await cache.trafficTypeExists('user_tt')).toBe(false); - }); test('killLocally', async () => { const wrapper = wrapperMockFactory(); const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper); - await cache.addSplit('lol1', splitWithUserTT); - await cache.addSplit('lol2', splitWithAccountTT); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); const initialChangeNumber = await cache.getChangeNumber(); // kill an non-existent split @@ -129,8 +113,8 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = await cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment', 100); + let lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -139,14 +123,15 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = await cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment_2', 90); + lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.removeSplits(['lol1', 'lol2']); + await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await wrapper.del(keysBuilder.buildSplitsTillKey()); expect(await wrapper.getKeysByPrefix('SPLITIO')).toHaveLength(0); }); @@ -155,12 +140,12 @@ describe('SPLITS CACHE PLUGGABLE', () => { const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new Set([]); - await cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + await cache.addSplit(featureFlagWithEmptyFS); expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -168,13 +153,13 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(await cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(await cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(await cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -189,7 +174,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(await cache.getNamesByFlagSets([])).toEqual([]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + await cache.addSplit({ ...featureFlagWithoutFS, name: featureFlagWithEmptyFS.name }); expect(await cache.getNamesByFlagSets([])).toEqual([]); }); @@ -198,12 +183,12 @@ describe('SPLITS CACHE PLUGGABLE', () => { const cacheWithoutFilters = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); const emptySet = new Set([]); - await cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree + ], [], -1); + await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(await cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index ee8b1872..cc16bceb 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -88,7 +88,8 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn // Connects to wrapper and emits SDK_READY event on main client const connectPromise = wrapper.connect().then(() => { if (isSynchronizer) { - // In standalone or producer mode, clear storage if SDK key or feature flag filter has changed + // @TODO reuse InLocalStorage::validateCache logic + // In standalone or producer mode, clear storage if SDK key, flags filter criteria or flags spec version was modified return wrapper.get(keys.buildHashKey()).then((hash) => { const currentHash = getStorageHash(settings); if (hash !== currentHash) { diff --git a/src/storages/types.ts b/src/storages/types.ts index 638c4606..a0ede2f1 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -177,11 +177,9 @@ export interface IPluggableStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): MaybeThenable, - removeSplits(names: string[]): MaybeThenable, + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec - setChangeNumber(changeNumber: number): MaybeThenable, // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage. getChangeNumber(): MaybeThenable, getAll(): MaybeThenable, @@ -191,42 +189,34 @@ export interface ISplitsCacheBase { // only for Client-Side. Returns true if the storage is not synchronized yet (getChangeNumber() === -1) or contains a FF using segments or large segments usesSegments(): MaybeThenable, clear(): MaybeThenable, - // should never reject or throw an exception. Instead return false by default, to avoid emitting SDK_READY_FROM_CACHE. - checkCache(): MaybeThenable, killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable, getNamesByFlagSets(flagSets: string[]): MaybeThenable[]> } export interface ISplitsCacheSync extends ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): boolean[], - removeSplits(names: string[]): boolean[], + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean, getSplit(name: string): ISplit | null, getSplits(names: string[]): Record, - setChangeNumber(changeNumber: number): boolean | void, getChangeNumber(): number, getAll(): ISplit[], getSplitNames(): string[], trafficTypeExists(trafficType: string): boolean, usesSegments(): boolean, clear(): void, - checkCache(): boolean, killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean, getNamesByFlagSets(flagSets: string[]): Set[] } export interface ISplitsCacheAsync extends ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): Promise, - removeSplits(names: string[]): Promise, + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, - setChangeNumber(changeNumber: number): Promise, getChangeNumber(): Promise, getAll(): Promise, getSplitNames(): Promise, trafficTypeExists(trafficType: string): Promise, usesSegments(): Promise, clear(): Promise, - checkCache(): Promise, killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise, getNamesByFlagSets(flagSets: string[]): Promise[]> } @@ -428,13 +418,13 @@ export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync, */ export interface IStorageBase< - TSplitsCache extends ISplitsCacheBase, - TSegmentsCache extends ISegmentsCacheBase, - TImpressionsCache extends IImpressionsCacheBase, - TImpressionsCountCache extends IImpressionCountsCacheBase, - TEventsCache extends IEventsCacheBase, - TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync, - TUniqueKeysCache extends IUniqueKeysCacheBase + TSplitsCache extends ISplitsCacheBase = ISplitsCacheBase, + TSegmentsCache extends ISegmentsCacheBase = ISegmentsCacheBase, + TImpressionsCache extends IImpressionsCacheBase = IImpressionsCacheBase, + TImpressionsCountCache extends IImpressionCountsCacheBase = IImpressionCountsCacheBase, + TEventsCache extends IEventsCacheBase = IEventsCacheBase, + TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync = ITelemetryCacheSync | ITelemetryCacheAsync, + TUniqueKeysCache extends IUniqueKeysCacheBase = IUniqueKeysCacheBase > { splits: TSplitsCache, segments: TSegmentsCache, @@ -457,6 +447,7 @@ export interface IStorageSync extends IStorageBase< IUniqueKeysCacheSync > { // Defined in client-side + validateCache?: () => boolean, // @TODO support async largeSegments?: ISegmentsCacheSync, } diff --git a/src/storages/utils.ts b/src/storages/utils.ts index 2963bbc5..49b21690 100644 --- a/src/storages/utils.ts +++ b/src/storages/utils.ts @@ -30,6 +30,7 @@ export function impressionsToJSON(impressions: SplitIO.ImpressionDTO[], metadata c: impression.changeNumber, m: impression.time, pt: impression.pt, + properties: impression.properties } }; diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 7fda853b..c7dba96e 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -2,6 +2,7 @@ import { fullSettings } from '../../utils/settingsValidation/__tests__/settings. import { syncTaskFactory } from './syncTask.mock'; import { syncManagerOnlineFactory } from '../syncManagerOnline'; import { IReadinessManager } from '../../readiness/types'; +import { SDK_SPLITS_CACHE_LOADED } from '../../readiness/constants'; jest.mock('../submitters/submitterManager', () => { return { @@ -45,8 +46,10 @@ const pushManagerFactoryMock = jest.fn(() => pushManagerMock); test('syncManagerOnline should start or not the submitter depending on user consent status', () => { const settings = { ...fullSettings }; - // @ts-ignore - const syncManager = syncManagerOnlineFactory()({ settings }); + const syncManager = syncManagerOnlineFactory()({ + settings, // @ts-ignore + storage: {}, + }); const submitterManager = syncManager.submitterManager!; syncManager.start(); @@ -95,7 +98,10 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () // @ts-ignore // Test pushManager for main client - const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ + settings, // @ts-ignore + storage: { validateCache: () => false }, + }); expect(pushManagerFactoryMock).not.toBeCalled(); @@ -161,7 +167,10 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () settings.sync.enabled = true; // @ts-ignore // pushManager instantiation control test - const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ + settings, // @ts-ignore + storage: { validateCache: () => false }, + }); expect(pushManagerFactoryMock).toBeCalled(); @@ -173,3 +182,18 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () testSyncManager.stop(); }); + +test('syncManagerOnline should emit SDK_SPLITS_CACHE_LOADED if validateCache returns true', async () => { + const params = { + settings: fullSettings, + storage: { validateCache: () => true }, + readiness: { splits: { emit: jest.fn() } } + }; // @ts-ignore + const syncManager = syncManagerOnlineFactory()(params); + + await syncManager.start(); + + expect(params.readiness.splits.emit).toBeCalledWith(SDK_SPLITS_CACHE_LOADED); + + syncManager.stop(); +}); diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 84805110..acbb5f52 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -1,6 +1,6 @@ import { forOwn } from '../../../utils/lang'; import { IReadinessManager } from '../../../readiness/types'; -import { ISplitsCacheSync } from '../../../storages/types'; +import { IStorageSync } from '../../../storages/types'; import { ISplitsParser } from '../splitsParser/types'; import { ISplit, ISplitPartial } from '../../../dtos/types'; import { syncTaskFactory } from '../../syncTask'; @@ -15,7 +15,7 @@ import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/c */ export function fromObjectUpdaterFactory( splitsParser: ISplitsParser, - storage: { splits: ISplitsCacheSync }, + storage: Pick, readiness: IReadinessManager, settings: ISettings, ): () => Promise { @@ -24,7 +24,7 @@ export function fromObjectUpdaterFactory( let startingUp = true; return function objectUpdater() { - const splits: [string, ISplit][] = []; + const splits: ISplit[] = []; let loadError = null; let splitsMock: false | Record = {}; try { @@ -37,32 +37,32 @@ export function fromObjectUpdaterFactory( if (!loadError && splitsMock) { log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]); - forOwn(splitsMock, function (val, name) { - splits.push([ // @ts-ignore Split changeNumber and seed is undefined in localhost mode - name, { - name, - status: 'ACTIVE', - killed: false, - trafficAllocation: 100, - defaultTreatment: CONTROL, - conditions: val.conditions || [], - configurations: val.configurations, - trafficTypeName: val.trafficTypeName - } - ]); + forOwn(splitsMock, (val, name) => { + // @ts-ignore Split changeNumber and seed is undefined in localhost mode + splits.push({ + name, + status: 'ACTIVE', + killed: false, + trafficAllocation: 100, + defaultTreatment: CONTROL, + conditions: val.conditions || [], + configurations: val.configurations, + trafficTypeName: val.trafficTypeName + }); }); return Promise.all([ splitsCache.clear(), // required to sync removed splits from mock - splitsCache.addSplits(splits) + splitsCache.update(splits, [], Date.now()) ]).then(() => { readiness.splits.emit(SDK_SPLITS_ARRIVED); if (startingUp) { startingUp = false; - Promise.resolve(splitsCache.checkCache()).then(cacheReady => { + const isCacheLoaded = storage.validateCache ? storage.validateCache() : false; + Promise.resolve().then(() => { // Emits SDK_READY_FROM_CACHE - if (cacheReady) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); + if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); // Emits SDK_READY readiness.segments.emit(SDK_SEGMENTS_ARRIVED); }); @@ -80,7 +80,7 @@ export function fromObjectUpdaterFactory( */ export function fromObjectSyncTaskFactory( splitsParser: ISplitsParser, - storage: { splits: ISplitsCacheSync }, + storage: Pick, readiness: IReadinessManager, settings: ISettings ): ISyncTask<[], boolean> { diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index baef383c..d6fed5a2 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -22,8 +22,7 @@ export function splitsSyncTaskFactory( splitChangesUpdaterFactory( settings.log, splitChangesFetcherFactory(fetchSplitChanges), - storage.splits, - storage.segments, + storage, settings.sync.__splitFiltersValidation, readiness.splits, settings.startup.requestTimeoutBeforeReady, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index b4dca3fe..d59c7013 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -96,59 +96,59 @@ test('splitChangesUpdater / compute splits mutation', () => { let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[activeSplitWithSegments.name, activeSplitWithSegments]]); - expect(splitsMutation.removed).toEqual([archivedSplit.name]); + expect(splitsMutation.added).toEqual([activeSplitWithSegments]); + expect(splitsMutation.removed).toEqual([archivedSplit]); expect(splitsMutation.segments).toEqual(['A', 'B']); // SDK initialization without sets // should process all the notifications splitsMutation = computeSplitsMutation([testFFSetsAB, test2FFSetsX] as ISplit[], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB],[test2FFSetsX.name, test2FFSetsX]]); + expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); expect(splitsMutation.segments).toEqual([]); }); test('splitChangesUpdater / compute splits mutation with filters', () => { // SDK initialization with sets: [set_a, set_b] - let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; + let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a', 'set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); // should add it to mutations - expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB]]); + expect(splitsMutation.added).toEqual([testFFSetsAB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFRemoveSetB], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[testFFRemoveSetB.name, testFFRemoveSetB]]); + expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFRemoveSetA], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFRemoveSetA.name]); + expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFEmptySet], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); + expect(splitsMutation.removed).toEqual([testFFEmptySet]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFSetsAB.name]); + expect(splitsMutation.removed).toEqual([testFFSetsAB]); splitsMutation = computeSplitsMutation([test2FFSetsX, testFFEmptySet], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[test2FFSetsX.name, test2FFSetsX],]); - expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); + expect(splitsMutation.added).toEqual([test2FFSetsX]); + expect(splitsMutation.removed).toEqual([testFFEmptySet]); }); describe('splitChangesUpdater', () => { @@ -158,19 +158,20 @@ describe('splitChangesUpdater', () => { const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splitsCache = new SplitsCacheInMemory(); - const setChangeNumber = jest.spyOn(splitsCache, 'setChangeNumber'); - const addSplits = jest.spyOn(splitsCache, 'addSplits'); - const removeSplits = jest.spyOn(splitsCache, 'removeSplits'); + const splits = new SplitsCacheInMemory(); + const updateSplits = jest.spyOn(splits, 'update'); + + const segments = new SegmentsCacheInMemory(); + const registerSegments = jest.spyOn(segments, 'registerSegments'); + + const storage = { splits, segments }; - const segmentsCache = new SegmentsCacheInMemory(); - const registerSegments = jest.spyOn(segmentsCache, 'registerSegments'); const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); let splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); + let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1); afterEach(() => { jest.clearAllMocks(); @@ -178,12 +179,8 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); - expect(setChangeNumber).toBeCalledTimes(1); - expect(setChangeNumber).lastCalledWith(splitChangesMock1.till); - expect(addSplits).toBeCalledTimes(1); - expect(addSplits.mock.calls[0][0].length).toBe(splitChangesMock1.splits.length); - expect(removeSplits).toBeCalledTimes(1); - expect(removeSplits).lastCalledWith([]); + expect(updateSplits).toBeCalledTimes(1); + expect(updateSplits).lastCalledWith(splitChangesMock1.splits, [], splitChangesMock1.till); expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); @@ -195,18 +192,16 @@ describe('splitChangesUpdater', () => { const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets:[]}, changeNumber: changeNumber })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); // fetch not being called expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(index + 1); // Change number being updated - expect(setChangeNumber).toBeCalledTimes(index + 1); - expect(setChangeNumber.mock.calls[index][0]).toEqual(changeNumber); + expect(updateSplits.mock.calls[index][2]).toEqual(changeNumber); // Add feature flag in notification - expect(addSplits).toBeCalledTimes(index + 1); - expect(addSplits.mock.calls[index][0].length).toBe(payload.status === ARCHIVED_FF ? 0 : 1); + expect(updateSplits.mock.calls[index][0].length).toBe(payload.status === ARCHIVED_FF ? 0 : 1); // Remove feature flag if status is ARCHIVED - expect(removeSplits).toBeCalledTimes(index + 1); - expect(removeSplits.mock.calls[index][0]).toEqual(payload.status === ARCHIVED_FF ? [payload.name] : []); + expect(updateSplits.mock.calls[index][1]).toEqual(payload.status === ARCHIVED_FF ? [payload] : []); // fetch segments after feature flag update expect(registerSegments).toBeCalledTimes(index + 1); expect(registerSegments.mock.calls[index][0]).toEqual(payload.status === ARCHIVED_FF ? [] : ['maur-2']); @@ -214,7 +209,7 @@ describe('splitChangesUpdater', () => { } }); - test('flag sets splits-arrived emition', async () => { + test('flag sets splits-arrived emission', async () => { const payload = splitNotifications[3].decoded as Pick; const setMocks = [ { sets: [], shouldEmit: false }, /* should not emit if flag does not have any set */ @@ -225,24 +220,25 @@ describe('splitChangesUpdater', () => { { sets: ['set_a'], shouldEmit: true }, /* should emit if flag is back in configured sets */ ]; - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); let index = 0; let calls = 0; // emit always if not configured sets for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); index++; } // @ts-ignore splitFiltersValidation = { queryString: null, groupedFilters: { bySet: ['set_a'], byName: [], byPrefix: [] }, validFilters: [] }; - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + storage.splits.clear(); + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); if (setMock.shouldEmit) calls++; expect(splitsEmitSpy.mock.calls.length).toBe(calls); index++; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index e8153987..7a341cd0 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,11 +1,11 @@ -import { ISegmentsCacheBase, ISplitsCacheBase } from '../../../storages/types'; +import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; -import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; +import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; -import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; import { IN_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; @@ -42,8 +42,8 @@ export function parseSegments({ conditions }: ISplit): Set { } interface ISplitMutations { - added: [string, ISplit][], - removed: string[], + added: ISplit[], + removed: ISplit[], segments: string[] } @@ -77,13 +77,13 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV const segments = new Set(); const computed = entries.reduce((accum, split) => { if (split.status === 'ACTIVE' && matchFilters(split, filters)) { - accum.added.push([split.name, split]); + accum.added.push(split); parseSegments(split).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(split.name); + accum.removed.push(split); } return accum; @@ -111,14 +111,14 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - splits: ISplitsCacheBase, - segments: ISegmentsCacheBase, + storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, isClientSide?: boolean ): ISplitChangesUpdater { + const { splits, segments } = storage; let startingUp = true; @@ -128,16 +128,6 @@ export function splitChangesUpdaterFactory( return promise; } - /** Returns true if at least one split was updated */ - function isThereUpdate(flagsChange: [boolean | void, void | boolean[], void | boolean[], boolean | void] | [any, any, any]) { - const [, added, removed] = flagsChange; - // There is at least one added or modified feature flag - if (added && added.some((update: boolean) => update)) return true; - // There is at least one removed feature flag - if (removed && removed.some((update: boolean) => update)) return true; - return false; - } - /** * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. * Returned promise will not be rejected. @@ -153,7 +143,7 @@ export function splitChangesUpdaterFactory( */ function _splitChangesUpdater(since: number, retry = 0): Promise { log.debug(SYNC_SPLITS_FETCH, [since]); - const fetcherPromise = Promise.resolve(splitUpdateNotification ? + return Promise.resolve(splitUpdateNotification ? { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } : splitChangesFetcher(since, noCache, till, _promiseDecorator) ) @@ -162,22 +152,15 @@ export function splitChangesUpdaterFactory( const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation); - log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); - log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); - log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); + log.debug(SYNC_SPLITS_UPDATE, [mutation.added.length, mutation.removed.length, mutation.segments.length]); - // Write into storage - // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency return Promise.all([ - // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed - splits.setChangeNumber(splitChanges.till), - splits.addSplits(mutation.added), - splits.removeSplits(mutation.removed), + splits.update(mutation.added, mutation.removed, splitChanges.till), segments.registerSegments(mutation.segments) - ]).then((flagsChange) => { + ]).then(([isThereUpdate]) => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate(flagsChange) && (isClientSide || checkAllSegmentsExist(segments)))) + return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events @@ -200,15 +183,6 @@ export function splitChangesUpdaterFactory( } return false; }); - - // After triggering the requests, if we have cached splits information let's notify that to emit SDK_READY_FROM_CACHE. - // Wrapping in a promise since checkCache can be async. - if (splitsEventEmitter && startingUp) { - Promise.resolve(splits.checkCache()).then(isCacheReady => { - if (isCacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); - }); - } - return fetcherPromise; } let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index d5fd3acd..4de69ca0 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -169,30 +169,30 @@ describe('SplitsUpdateWorker', () => { test('killSplit', async () => { // setup const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', '{ "name": "something"}'); - cache.addSplit('lol2', '{ "name": "something else"}'); + cache.addSplit({ name: 'something'}); + cache.addSplit({ name: 'something else'}); const splitsSyncTask = splitsSyncTaskMock(cache); const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new - splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'lol1', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous + splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'something', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false expect(splitsEventEmitterMock.emit.mock.calls).toEqual([[SDK_SPLITS_ARRIVED, true]]); // emits `SDK_SPLITS_ARRIVED` with `isSplitKill` flag in true, if split kill resolves with update - assertKilledSplit(cache, 100, 'lol1', 'off'); + assertKilledSplit(cache, 100, 'something', 'off'); // assert not killing split locally, not emitting SDK_SPLITS_ARRIVED event, and not synchronizes splits, if changeNumber is old splitsSyncTask.__resolveSplitsUpdaterCall(100); await new Promise(res => setTimeout(res)); splitsSyncTask.execute.mockClear(); splitsEventEmitterMock.emit.mockClear(); - splitUpdateWorker.killSplit({ changeNumber: 90, splitName: 'lol1', defaultTreatment: 'on' }); + splitUpdateWorker.killSplit({ changeNumber: 90, splitName: 'something', defaultTreatment: 'on' }); await new Promise(res => setTimeout(res)); expect(splitsSyncTask.execute).toBeCalledTimes(0); // doesn't synchronize splits if killLocally resolved without update expect(splitsEventEmitterMock.emit).toBeCalledTimes(0); // doesn't emit `SDK_SPLITS_ARRIVED` if killLocally resolved without update - assertKilledSplit(cache, 100, 'lol1', 'off'); // calling `killLocally` with an old changeNumber made no effect + assertKilledSplit(cache, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect }); test('stop', async () => { diff --git a/src/sync/submitters/impressionsSubmitter.ts b/src/sync/submitters/impressionsSubmitter.ts index 193e2703..bf05a587 100644 --- a/src/sync/submitters/impressionsSubmitter.ts +++ b/src/sync/submitters/impressionsSubmitter.ts @@ -25,8 +25,9 @@ export function fromImpressionsCollector(sendLabels: boolean, data: SplitIO.Impr m: entry.time, // Timestamp c: entry.changeNumber, // ChangeNumber r: sendLabels ? entry.label : undefined, // Rule - b: entry.bucketingKey ? entry.bucketingKey : undefined, // Bucketing Key - pt: entry.pt ? entry.pt : undefined // Previous time + b: entry.bucketingKey, // Bucketing Key + pt: entry.pt, // Previous time + properties: entry.properties // Properties }; return keyImpression; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index f3b93c4d..9bae212e 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -3,26 +3,30 @@ import { IMetadata } from '../../dtos/types'; import SplitIO from '../../../types/splitio'; import { ISyncTask } from '../types'; +type ImpressionPayload = { + /** Matching Key */ + k: string; + /** Bucketing Key */ + b?: string; + /** Treatment */ + t: string; + /** Timestamp */ + m: number; + /** Change number */ + c: number; + /** Rule label */ + r?: string; + /** Previous time */ + pt?: number; + /** Stringified JSON object with properties */ + properties?: string; +}; + export type ImpressionsPayload = { /** Split name */ f: string, /** Key Impressions */ - i: { - /** User Key */ - k: string; - /** Treatment */ - t: string; - /** Timestamp */ - m: number; - /** ChangeNumber */ - c: number; - /** Rule label */ - r?: string; - /** Bucketing Key */ - b?: string; - /** Previous time */ - pt?: number; - }[] + i: ImpressionPayload[] }[] export type ImpressionCountsPayload = { @@ -60,23 +64,9 @@ export type StoredImpressionWithMetadata = { /** Metadata */ m: IMetadata, /** Stored impression */ - i: { - /** keyName */ - k: string, - /** bucketingKey */ - b?: string, - /** Split name */ - f: string, - /** treatment */ - t: string, - /** label */ - r: string, - /** changeNumber */ - c: number, - /** time */ - m: number - /** previous time */ - pt?: number + i: ImpressionPayload & { + /** Feature flag name */ + f: string } } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 5410c17f..aed32493 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -9,6 +9,7 @@ import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '.. import { isConsentGranted } from '../consent'; import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; import { ISdkFactoryContextSync } from '../sdkFactory/types'; +import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants'; /** * Online SyncManager factory. @@ -28,7 +29,7 @@ export function syncManagerOnlineFactory( */ return function (params: ISdkFactoryContextSync): ISyncManagerCS { - const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params; + const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker, storage, readiness } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); @@ -87,6 +88,11 @@ export function syncManagerOnlineFactory( start() { running = true; + if (startFirstTime) { + const isCacheLoaded = storage.validateCache ? storage.validateCache() : false; + if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); }); + } + // start syncing splits and segments if (pollingManager) { @@ -96,7 +102,6 @@ export function syncManagerOnlineFactory( // Doesn't call `syncAll` when the syncManager is resuming if (startFirstTime) { pollingManager.syncAll(); - startFirstTime = false; } pushManager.start(); } else { @@ -105,13 +110,14 @@ export function syncManagerOnlineFactory( } else { if (startFirstTime) { pollingManager.syncAll(); - startFirstTime = false; } } } // start periodic data recording (events, impressions, telemetry). submitterManager.start(!isConsentGranted(settings)); + + startFirstTime = false; }, /** diff --git a/src/trackers/strategy/strategyDebug.ts b/src/trackers/strategy/strategyDebug.ts index 65bc06b3..ae19973e 100644 --- a/src/trackers/strategy/strategyDebug.ts +++ b/src/trackers/strategy/strategyDebug.ts @@ -14,6 +14,8 @@ export function strategyDebugFactory( return { process(impression: SplitIO.ImpressionDTO) { + if (impression.properties) return true; + impression.pt = impressionsObserver.testAndSet(impression); return true; } diff --git a/src/trackers/strategy/strategyOptimized.ts b/src/trackers/strategy/strategyOptimized.ts index 9a9cf883..24c82ebd 100644 --- a/src/trackers/strategy/strategyOptimized.ts +++ b/src/trackers/strategy/strategyOptimized.ts @@ -18,6 +18,9 @@ export function strategyOptimizedFactory( return { process(impression: SplitIO.ImpressionDTO) { + // DEBUG mode without previous time, for impressions with properties + if (impression.properties) return true; + impression.pt = impressionsObserver.testAndSet(impression); const now = Date.now(); diff --git a/src/utils/constants/browser.ts b/src/utils/constants/browser.ts deleted file mode 100644 index d627f780..00000000 --- a/src/utils/constants/browser.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This value might be eventually set via a config parameter -export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 310946cc..1306431c 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -66,3 +66,13 @@ export function validateEventProperties(log: ILogger, maybeProperties: any, meth return output; } + +export function validateEvaluationOptions(log: ILogger, maybeOptions: any, method: string): SplitIO.EvaluationOptions | undefined { + if (isObject(maybeOptions)) { + const properties = validateEventProperties(log, maybeOptions.properties, method).properties; + return properties && Object.keys(properties).length > 0 ? { properties } : undefined; + } else if (maybeOptions) { + log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'evaluation options']); + } + return undefined; +} diff --git a/src/utils/inputValidation/index.ts b/src/utils/inputValidation/index.ts index 93b09ab7..96cf4be6 100644 --- a/src/utils/inputValidation/index.ts +++ b/src/utils/inputValidation/index.ts @@ -11,3 +11,4 @@ export { validateIfNotDestroyed, validateIfOperational } from './isOperational'; export { validateSplitExistence } from './splitExistence'; export { validateTrafficTypeExistence } from './trafficTypeExistence'; export { validatePreloadedData } from './preloadedData'; +export { validateEvaluationOptions } from './eventProperties'; diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 11b6afd0..b1a7e35a 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -120,7 +120,7 @@ export function isBoolean(val: any): boolean { * Unlike `Number.isFinite`, it also tests Number object instances. * Unlike global `isFinite`, it returns false if the value is not a number or Number object instance. */ -export function isFiniteNumber(val: any): boolean { +export function isFiniteNumber(val: any): val is number { if (val instanceof Number) val = val.valueOf(); return typeof val === 'number' ? Number.isFinite ? Number.isFinite(val) : isFinite(val) : diff --git a/src/utils/settingsValidation/storage/storageCS.ts b/src/utils/settingsValidation/storage/storageCS.ts index f7b531fc..7d58af3d 100644 --- a/src/utils/settingsValidation/storage/storageCS.ts +++ b/src/utils/settingsValidation/storage/storageCS.ts @@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types'; export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync { const result = InMemoryStorageCSFactory(params); - result.splits.checkCache = () => true; // to emit SDK_READY_FROM_CACHE + result.validateCache = () => true; // to emit SDK_READY_FROM_CACHE return result; } __InLocalStorageMockFactory.type = STORAGE_MEMORY; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0cab4b66..d8a1e67d 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -778,6 +778,15 @@ declare namespace SplitIO { type Properties = { [propertyName: string]: string | number | boolean | null; }; + /** + * Evaluation options object for getTreatment methods. + */ + type EvaluationOptions = { + /** + * Optional properties to append to the generated impression object sent to Split backend. + */ + properties?: Properties; + } /** * The SplitKey object format. */ @@ -803,14 +812,42 @@ declare namespace SplitIO { * Impression DTO generated by the SDK when processing evaluations. */ type ImpressionDTO = { + /** + * Feature flag name. + */ feature: string; + /** + * Key. + */ keyName: string; + /** + * Treatment value. + */ treatment: string; + /** + * Impression timestamp. + */ time: number; + /** + * Bucketing Key + */ bucketingKey?: string; + /** + * Rule label + */ label: string; + /** + * Version of the feature flag + */ changeNumber: number; + /** + * Previous time + */ pt?: number; + /** + * JSON stringified version of the impression properties. + */ + properties?: string; } /** * Object with information about an impression. It contains the generated impression DTO as well as @@ -910,6 +947,18 @@ declare namespace SplitIO { * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings to clear the cache. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; } /** * Storage for asynchronous (consumer) SDK. @@ -1233,11 +1282,23 @@ declare namespace SplitIO { */ type?: BrowserStorage; /** - * Optional prefix to prevent any kind of data collision between SDK versions. + * Optional prefix to prevent any kind of data collision between SDK versions when using 'LOCALSTORAGE'. * * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; }; } /** @@ -1514,73 +1575,80 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatment string. */ - getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes): Treatment; + getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; /** * Returns a TreatmentWithConfig value, which is an object with both treatment and config string for the given feature. * * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. - * @returns The TreatmentWithConfig, the object containing the treatment string and the - * configuration stringified JSON (or null if there was no config for that treatment). + * @param options - An object of type EvaluationOptions for advanced evaluation options. + * @returns The TreatmentWithConfig object that contains the treatment string and the configuration stringified JSON (or null if there was no config for that treatment). */ - getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes): TreatmentWithConfig; + getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the given features. * * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatments object map. */ - getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): Treatments; + getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatment objects */ - getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): Treatments; + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatment objects */ - getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): Treatments; + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface. * @@ -1605,72 +1673,80 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatment promise that resolves to the treatment string. */ - getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes): AsyncTreatment; + getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; /** * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for the given feature. * * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ - getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes): AsyncTreatmentWithConfig; + getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; /** * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the given features. * * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): AsyncTreatments; + getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which will be (or eventually be) an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface, and returns a promise to signal when the event was successfully queued (or not). * @@ -1735,65 +1811,73 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatment string. */ - getTreatment(featureFlagName: string, attributes?: Attributes): Treatment; + getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; /** * Returns a TreatmentWithConfig value, which is an object with both treatment and config string for the given feature. * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. - * @returns The map containing the treatment and the configuration stringified JSON (or null if there was no config for that treatment). + * @param options - An object of type EvaluationOptions for advanced evaluation options. + * @returns The TreatmentWithConfig object that contains the treatment string and the configuration stringified JSON (or null if there was no config for that treatment). */ - getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes): TreatmentWithConfig; + getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the given features. * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatments object map. */ - getTreatments(featureFlagNames: string[], attributes?: Attributes): Treatments; + getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatments objects */ - getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes): Treatments; + getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatments objects */ - getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes): Treatments; + getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface. * @@ -1814,65 +1898,73 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatment promise that resolves to the treatment string. */ - getTreatment(featureFlagName: string, attributes?: Attributes): AsyncTreatment; + getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; /** * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for the given feature. * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ - getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes): AsyncTreatmentWithConfig; + getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; /** * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the given features. * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatments(featureFlagNames: string[], attributes?: Attributes): AsyncTreatments; + getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which will be (or eventually be) an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface, and returns a promise to signal when the event was successfully queued (or not). *