From 29844ffe814f428b51f2e74f7e49eb8601b44b60 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 5 Sep 2024 11:09:52 -0300 Subject: [PATCH 01/25] Upgrade JS SDK and vulnerability fixes --- package-lock.json | 121 +++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 52 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76a4c02..3c6a64c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.13.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.26.0", + "@splitsoftware/splitio": "10.28.0-rc.3", "tslib": "^2.3.1" }, "devDependencies": { @@ -1503,11 +1503,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.26.0.tgz", - "integrity": "sha512-sACjAcov/Zn1gYaN6m0qQb9G/LDk43c8rEzFaabhlnWOsH0W22ImVHGx8iU3I/DyC1S2wrsjXTSnW1GQlbb7+Q==", + "version": "10.28.0-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0-rc.3.tgz", + "integrity": "sha512-ezcq4/7jVmpQ4gPGs6w90XkiTcuh//TKqqXYEJ6tDZVXZUukTVWbxSN1m1fZYpHaOI0nZtsZ0l2wKPlyAxihCA==", "dependencies": { - "@splitsoftware/splitio-commons": "1.14.0", + "@splitsoftware/splitio-commons": "1.17.0-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1520,15 +1520,12 @@ "engines": { "node": ">=6", "npm": ">=3" - }, - "optionalDependencies": { - "eventsource": "^1.1.2" } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.14.0.tgz", - "integrity": "sha512-ANP0NRPAMehi4bUQsb19kP5W5NVuCYUKRsDC5Nl78xHIu6cskAej1rXkjsocLnWerz2rO0H9kMjRKZj9lVsvKA==", + "version": "1.17.0-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0-rc.3.tgz", + "integrity": "sha512-DGApC3fH0qu6bNykf+mB/t5sgaMKX0WHWLpWm/mLFUsk7aNf2aPwxVU7qLlfYjPX0BcbXHCk2+LTJqYAcxXG9g==", "dependencies": { "tslib": "^2.3.1" }, @@ -2705,12 +2702,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4227,15 +4224,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventsource": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz", - "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -4369,9 +4357,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -7744,13 +7732,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -8252,9 +8240,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -9866,9 +9854,9 @@ } }, "node_modules/ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -11056,15 +11044,14 @@ } }, "@splitsoftware/splitio": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.26.0.tgz", - "integrity": "sha512-sACjAcov/Zn1gYaN6m0qQb9G/LDk43c8rEzFaabhlnWOsH0W22ImVHGx8iU3I/DyC1S2wrsjXTSnW1GQlbb7+Q==", + "version": "10.28.0-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0-rc.3.tgz", + "integrity": "sha512-ezcq4/7jVmpQ4gPGs6w90XkiTcuh//TKqqXYEJ6tDZVXZUukTVWbxSN1m1fZYpHaOI0nZtsZ0l2wKPlyAxihCA==", "requires": { - "@splitsoftware/splitio-commons": "1.14.0", + "@splitsoftware/splitio-commons": "1.17.0-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", - "eventsource": "^1.1.2", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", "node-fetch": "^2.7.0", @@ -11073,9 +11060,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.14.0.tgz", - "integrity": "sha512-ANP0NRPAMehi4bUQsb19kP5W5NVuCYUKRsDC5Nl78xHIu6cskAej1rXkjsocLnWerz2rO0H9kMjRKZj9lVsvKA==", + "version": "1.17.0-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0-rc.3.tgz", + "integrity": "sha512-DGApC3fH0qu6bNykf+mB/t5sgaMKX0WHWLpWm/mLFUsk7aNf2aPwxVU7qLlfYjPX0BcbXHCk2+LTJqYAcxXG9g==", "requires": { "tslib": "^2.3.1" } @@ -11969,12 +11956,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-process-hrtime": { @@ -13108,12 +13095,6 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "eventsource": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz", - "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==", - "optional": true - }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -13227,9 +13208,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -15717,13 +15698,13 @@ "dev": true }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "mime-db": { @@ -16096,9 +16077,9 @@ "dev": true }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pirates": { @@ -17313,9 +17294,9 @@ } }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 9da3da2..edccca6 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "homepage": "https://github.com/splitio/redux-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.26.0", + "@splitsoftware/splitio": "10.28.0-rc.3", "tslib": "^2.3.1" }, "devDependencies": { From cccc0868936450c1e987ad35b206d85713b9df4b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Sep 2024 16:18:46 -0300 Subject: [PATCH 02/25] Update tests and polishing --- CHANGES.txt | 6 +++ package-lock.json | 30 +++++++-------- package.json | 2 +- src/__tests__/helpers.browser.test.ts | 4 +- src/__tests__/helpers.node.test.ts | 2 +- src/__tests__/selectorsWithStatus.test.ts | 8 ++-- src/__tests__/utils/mockBrowserSplitSdk.ts | 43 ++++++++++++++-------- src/__tests__/utils/mockNodeSplitSdk.ts | 37 ++++++++++++------- src/__tests__/utils/storeState.ts | 4 +- src/helpers.ts | 8 +--- src/reducer.ts | 12 ++++-- src/types.ts | 20 +++++----- src/utils.ts | 4 +- 13 files changed, 105 insertions(+), 75 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0346c5e..fc49236 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +1.14.0 (September XX, 2024) + - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). + - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: + - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. + - Updated some transitive dependencies for vulnerability fixes. + 1.13.0 (May 24, 2024) - Added a new `getStatus` helper function to retrieve the status properties of the SDK manager and clients: `isReady`, `isReadyFromCache`, `hasTimedout`, and `isDestroyed`. - Added new `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors as alternatives to the `selectTreatmentValue` and `selectTreatmentWithConfig` selectors, respectively. diff --git a/package-lock.json b/package-lock.json index 3c6a64c..7efe18b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.13.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.28.0-rc.3", + "@splitsoftware/splitio": "10.28.0", "tslib": "^2.3.1" }, "devDependencies": { @@ -1503,11 +1503,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.28.0-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0-rc.3.tgz", - "integrity": "sha512-ezcq4/7jVmpQ4gPGs6w90XkiTcuh//TKqqXYEJ6tDZVXZUukTVWbxSN1m1fZYpHaOI0nZtsZ0l2wKPlyAxihCA==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", + "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", "dependencies": { - "@splitsoftware/splitio-commons": "1.17.0-rc.3", + "@splitsoftware/splitio-commons": "1.17.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1523,9 +1523,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.17.0-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0-rc.3.tgz", - "integrity": "sha512-DGApC3fH0qu6bNykf+mB/t5sgaMKX0WHWLpWm/mLFUsk7aNf2aPwxVU7qLlfYjPX0BcbXHCk2+LTJqYAcxXG9g==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", + "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", "dependencies": { "tslib": "^2.3.1" }, @@ -11044,11 +11044,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.28.0-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0-rc.3.tgz", - "integrity": "sha512-ezcq4/7jVmpQ4gPGs6w90XkiTcuh//TKqqXYEJ6tDZVXZUukTVWbxSN1m1fZYpHaOI0nZtsZ0l2wKPlyAxihCA==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", + "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", "requires": { - "@splitsoftware/splitio-commons": "1.17.0-rc.3", + "@splitsoftware/splitio-commons": "1.17.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -11060,9 +11060,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.17.0-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0-rc.3.tgz", - "integrity": "sha512-DGApC3fH0qu6bNykf+mB/t5sgaMKX0WHWLpWm/mLFUsk7aNf2aPwxVU7qLlfYjPX0BcbXHCk2+LTJqYAcxXG9g==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", + "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", "requires": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index edccca6..c8ffe06 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "homepage": "https://github.com/splitio/redux-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.28.0-rc.3", + "@splitsoftware/splitio": "10.28.0", "tslib": "^2.3.1" }, "devDependencies": { diff --git a/src/__tests__/helpers.browser.test.ts b/src/__tests__/helpers.browser.test.ts index feabf04..02b8fff 100644 --- a/src/__tests__/helpers.browser.test.ts +++ b/src/__tests__/helpers.browser.test.ts @@ -192,13 +192,13 @@ describe('getStatus', () => { (splitSdk.factory as any).client('user_2').__emitter__.emit(Event.SDK_READY_FROM_CACHE); // Main client - const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true }; + const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate }; expect(getStatus()).toEqual(MAIN_CLIENT_STATUS); expect(getStatus(sdkBrowserConfig.core.key)).toEqual(MAIN_CLIENT_STATUS); expect(getStatus({ matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: '' })).toEqual(MAIN_CLIENT_STATUS); // Client for user_2 - const USER_2_STATUS = { ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true }; + const USER_2_STATUS = { ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client('user_2') as any).__getStatus().lastUpdate }; expect(getStatus('user_2')).toEqual(USER_2_STATUS); expect(getStatus({ matchingKey: 'user_2', bucketingKey: '' })).toEqual(USER_2_STATUS); diff --git a/src/__tests__/helpers.node.test.ts b/src/__tests__/helpers.node.test.ts index 9f90fcf..3741352 100644 --- a/src/__tests__/helpers.node.test.ts +++ b/src/__tests__/helpers.node.test.ts @@ -160,7 +160,7 @@ describe('getStatus', () => { initSplitSdk({ config: sdkNodeConfig }); (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); - const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true }; + const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate }; expect(getStatus()).toEqual(MAIN_CLIENT_STATUS); expect(getStatus('ignored_key_in_server_side')).toEqual(MAIN_CLIENT_STATUS); }); diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index bf3a7d6..2398b24 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -52,7 +52,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ treatment: CONTROL, // status of main client: - ...STATUS_INITIAL, isReady: true, isOperational: true, + ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate, }); expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ @@ -67,7 +67,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ treatment: CONTROL_WITH_CONFIG, // status of shared client: - ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, + ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate, }); expect(errorSpy).not.toHaveBeenCalled(); @@ -85,13 +85,13 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({ treatment: ON, ...STATUS_INITIAL, - isReady: true, isOperational: true, + isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate }); expect(selectTreatmentWithConfigAndStatus(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], ...STATUS_INITIAL, - isReadyFromCache: true, isOperational: true, + isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate }); expect(errorSpy).not.toHaveBeenCalled(); diff --git a/src/__tests__/utils/mockBrowserSplitSdk.ts b/src/__tests__/utils/mockBrowserSplitSdk.ts index f6c696e..c5567fd 100644 --- a/src/__tests__/utils/mockBrowserSplitSdk.ts +++ b/src/__tests__/utils/mockBrowserSplitSdk.ts @@ -32,14 +32,22 @@ export function mockSdk() { function mockClient(key?: SplitIO.SplitKey) { // Readiness - let __isReady__: boolean | undefined; - let __isReadyFromCache__: boolean | undefined; - let __hasTimedout__: boolean | undefined; - let __isDestroyed__: boolean | undefined; + let isReady = false; + let isReadyFromCache = false; + let hasTimedout = false; + let isDestroyed = false; + let lastUpdate = 0; + + function syncLastUpdate() { + const dateNow = Date.now(); + lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; + } + const __emitter__ = new EventEmitter(); - __emitter__.once(Event.SDK_READY, () => { __isReady__ = true; }); - __emitter__.once(Event.SDK_READY_FROM_CACHE, () => { __isReadyFromCache__ = true; }); - __emitter__.once(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; }); + __emitter__.once(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); + __emitter__.once(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); + __emitter__.once(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); // Client methods const track: jest.Mock = jest.fn((tt, et, v, p) => { @@ -77,19 +85,22 @@ export function mockSdk() { }); const ready: jest.Mock = jest.fn(() => { return promiseWrapper(new Promise((res, rej) => { - __isReady__ ? res() : __emitter__.on(Event.SDK_READY, res); - __hasTimedout__ ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); + isReady ? res() : __emitter__.on(Event.SDK_READY, res); + hasTimedout ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); }), () => { }); }); const __getStatus = () => ({ - isReady: __isReady__ || false, - isReadyFromCache: __isReadyFromCache__ || false, - hasTimedout: __hasTimedout__ || false, - isDestroyed: __isDestroyed__ || false, - isOperational: ((__isReady__ || __isReadyFromCache__) && !__isDestroyed__) || false, + isReady, + isReadyFromCache, + isTimedout: hasTimedout && !isReady, + hasTimedout, + isDestroyed, + isOperational: (isReady || isReadyFromCache) && !isDestroyed, + lastUpdate, }); const destroy: jest.Mock = jest.fn(() => { - __isDestroyed__ = true; + isDestroyed = true; + syncLastUpdate(); return new Promise((res) => { setTimeout(res, 100); }); }); @@ -105,7 +116,7 @@ export function mockSdk() { getAttributes, // EventEmitter exposed to trigger events manually __emitter__, - // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) + // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, isOperational, lastUpdate) __getStatus, }); } diff --git a/src/__tests__/utils/mockNodeSplitSdk.ts b/src/__tests__/utils/mockNodeSplitSdk.ts index a5eee41..874c32a 100644 --- a/src/__tests__/utils/mockNodeSplitSdk.ts +++ b/src/__tests__/utils/mockNodeSplitSdk.ts @@ -10,12 +10,20 @@ export const Event = { function mockClient() { // Readiness - let __isReady__: boolean | undefined; - let __hasTimedout__: boolean | undefined; - let __isDestroyed__: boolean | undefined; + let isReady = false; + let hasTimedout = false; + let isDestroyed = false; + let lastUpdate = 0; + + function syncLastUpdate() { + const dateNow = Date.now(); + lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; + } + const __emitter__ = new EventEmitter(); - __emitter__.once(Event.SDK_READY, () => { __isReady__ = true; }); - __emitter__.once(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; }); + __emitter__.once(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); + __emitter__.once(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); // Client methods const track: jest.Mock = jest.fn(() => { @@ -35,19 +43,22 @@ function mockClient() { }); const ready: jest.Mock = jest.fn(() => { return promiseWrapper(new Promise((res, rej) => { - __isReady__ ? res() : __emitter__.on(Event.SDK_READY, res); - __hasTimedout__ ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); + isReady ? res() : __emitter__.on(Event.SDK_READY, res); + hasTimedout ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); }), () => { }); }); const __getStatus = () => ({ - isReady: __isReady__ || false, + isReady, isReadyFromCache: false, - hasTimedout: __hasTimedout__ || false, - isDestroyed: __isDestroyed__ || false, - isOperational: (__isReady__ && !__isDestroyed__) || false, + isTimedout: hasTimedout && !isReady, + hasTimedout, + isDestroyed, + isOperational: isReady && !isDestroyed, + lastUpdate, }); const destroy: jest.Mock = jest.fn(() => { - __isDestroyed__ = true; + isDestroyed = true; + syncLastUpdate(); return new Promise((res) => { setTimeout(res, 100); }); }); @@ -60,7 +71,7 @@ function mockClient() { Event, // EventEmitter exposed to trigger events manually __emitter__, - // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) + // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, isOperational, lastUpdate) __getStatus, }); } diff --git a/src/__tests__/utils/storeState.ts b/src/__tests__/utils/storeState.ts index b4af869..3cc9851 100644 --- a/src/__tests__/utils/storeState.ts +++ b/src/__tests__/utils/storeState.ts @@ -12,15 +12,15 @@ export const USER_INVALID = 'user_invalid'; export const STATUS_INITIAL = { isReady: false, isReadyFromCache: false, + isTimedout: false, hasTimedout: false, isDestroyed: false, + lastUpdate: 0, }; export const STATE_INITIAL: { splitio: ISplitState } = { splitio: { ...STATUS_INITIAL, - isTimedout: false, - lastUpdate: 0, treatments: { }, }, diff --git a/src/helpers.ts b/src/helpers.ts index 1b35fcf..bd89a12 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,6 +2,7 @@ import { splitSdk, getClient } from './asyncActions'; import { IStatus, ITrackParams } from './types'; import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK } from './constants'; import { __getStatus, matching } from './utils'; +import { initialStatus } from './reducer'; /** * This function track events, i.e., it invokes the actual `client.track*` methods. @@ -110,10 +111,5 @@ export function getStatus(key?: SplitIO.SplitKey): IStatus { } // Default status if SDK is not initialized or client is not found. No warning logs for now, in case the helper is used before actions are dispatched - return { - isReady: false, - isReadyFromCache: false, - hasTimedout: false, - isDestroyed: false, - }; + return { ...initialStatus }; } diff --git a/src/reducer.ts b/src/reducer.ts index 2b930de..89ac4e6 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -5,16 +5,20 @@ import { SPLIT_UPDATE, SPLIT_UPDATE_WITH_EVALUATIONS, SPLIT_TIMEDOUT, SPLIT_DESTROY, ADD_TREATMENTS, } from './constants'; -/** - * Initial default state for Split reducer - */ -const initialState: ISplitState = { +export const initialStatus = { isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, lastUpdate: 0, +} + +/** + * Initial default state for Split reducer + */ +const initialState: ISplitState = { + ...initialStatus, treatments: {}, }; diff --git a/src/types.ts b/src/types.ts index 6af78ec..60b0711 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,13 @@ export interface IStatus { */ isReadyFromCache: boolean; + /** + * isTimedout indicates if the Split client has emitted an SDK_READY_TIMED_OUT event and is not ready. + * In other words, `isTimedout` is equivalent to `hasTimeout && !isReady`. + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + */ + isTimedout: boolean; + /** * hasTimedout indicates if the Split client has ever emitted an SDK_READY_TIMED_OUT event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. @@ -28,23 +35,16 @@ export interface IStatus { * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#shutdown} */ isDestroyed: boolean; -} - -/** Type for Split reducer's slice of state */ -export interface ISplitState extends IStatus { - - /** - * isTimedout indicates if the Split client has emitted an SDK_READY_TIMED_OUT event and is not ready. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} - */ - isTimedout: boolean; /** * lastUpdate is the timestamp of the last Split client event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} */ lastUpdate: number; +} +/** Type for Split reducer's slice of state */ +export interface ISplitState extends IStatus { /** * `treatments` is a nested object property that contains the evaluations of feature flags. * Each evaluation (treatment) is associated with a feature flag name and a key (e.g., unique user identifier, such as a user id). diff --git a/src/utils.ts b/src/utils.ts index bdf915d..440e7e3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,9 +23,11 @@ export function matching(key: SplitIO.SplitKey): string { export interface IClientStatus { isReady: boolean; isReadyFromCache: boolean; - isOperational: boolean; + isTimedout: boolean; hasTimedout: boolean; isDestroyed: boolean; + isOperational: boolean; + lastUpdate: number; } export function __getStatus(client: SplitIO.IClient): IClientStatus { From 5cab1e2207738f6851fe22c270691ee7c04e6495 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Sep 2024 16:53:49 -0300 Subject: [PATCH 03/25] Refactor splitReducer --- src/reducer.ts | 64 ++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/reducer.ts b/src/reducer.ts index 89ac4e6..6778e07 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -1,5 +1,5 @@ import { Reducer } from 'redux'; -import { ISplitState } from './types'; +import { ISplitState, IStatus } from './types'; import { SPLIT_READY, SPLIT_READY_WITH_EVALUATIONS, SPLIT_READY_FROM_CACHE, SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_UPDATE, SPLIT_UPDATE_WITH_EVALUATIONS, SPLIT_TIMEDOUT, SPLIT_DESTROY, ADD_TREATMENTS, @@ -22,28 +22,47 @@ const initialState: ISplitState = { treatments: {}, }; -function setReady(state: ISplitState, timestamp: number) { +function setStatus(state: ISplitState, patch: Partial) { return { ...state, + ...patch, + }; +} + +function setReady(state: ISplitState, timestamp: number) { + return setStatus(state, { isReady: true, isTimedout: false, lastUpdate: timestamp, - }; + }); } function setReadyFromCache(state: ISplitState, timestamp: number) { - return { - ...state, + return setStatus(state, { isReadyFromCache: true, lastUpdate: timestamp, - }; + }); +} + +function setTimedout(state: ISplitState, timestamp: number) { + return setStatus(state, { + isTimedout: true, + hasTimedout: true, + lastUpdate: timestamp, + }); } function setUpdated(state: ISplitState, timestamp: number) { - return { - ...state, + return setStatus(state, { lastUpdate: timestamp, - }; + }); +} + +function setDestroyed(state: ISplitState, timestamp: number) { + return setStatus(state, { + isDestroyed: true, + lastUpdate: timestamp, + }); } /** @@ -75,51 +94,40 @@ export const splitReducer: Reducer = function ( state = initialState, action, ) { - switch (action.type) { + const { type, payload: { timestamp, key, treatments } = {} as any } = action as any; + + switch (type) { case SPLIT_READY: - return setReady(state, (action as any).payload.timestamp); + return setReady(state, timestamp); case SPLIT_READY_FROM_CACHE: - return setReadyFromCache(state, (action as any).payload.timestamp); + return setReadyFromCache(state, timestamp); case SPLIT_TIMEDOUT: - return { - ...state, - isTimedout: true, - hasTimedout: true, - lastUpdate: (action as any).payload.timestamp, - }; + return setTimedout(state, timestamp); case SPLIT_UPDATE: - return setUpdated(state, (action as any).payload.timestamp); + return setUpdated(state, timestamp); case SPLIT_DESTROY: - return { - ...state, - isDestroyed: true, - lastUpdate: (action as any).payload.timestamp, - }; + return setDestroyed(state, timestamp); case ADD_TREATMENTS: { - const { key, treatments } = (action as any).payload; const result = { ...state }; return assignTreatments(result, key, treatments); } case SPLIT_READY_WITH_EVALUATIONS: { - const { key, treatments, timestamp } = (action as any).payload; const result = setReady(state, timestamp); return assignTreatments(result, key, treatments); } case SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS: { - const { key, treatments, timestamp } = (action as any).payload; const result = setReadyFromCache(state, timestamp); return assignTreatments(result, key, treatments); } case SPLIT_UPDATE_WITH_EVALUATIONS: { - const { key, treatments, timestamp } = (action as any).payload; const result = setUpdated(state, timestamp); return assignTreatments(result, key, treatments); } From 057d2956533094ec97617984fc78c65df064386e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Sep 2024 17:24:42 -0300 Subject: [PATCH 04/25] Use SDK client lastUpdate to set the property in the state --- src/__tests__/actions.browser.test.ts | 12 ++++---- src/__tests__/actions.node.test.ts | 8 ++--- src/__tests__/reducer.test.ts | 44 +++++++++++++-------------- src/actions.ts | 32 +++++++++---------- src/asyncActions.ts | 33 ++++++++++++-------- src/utils.ts | 4 +-- 6 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/__tests__/actions.browser.test.ts b/src/__tests__/actions.browser.test.ts index 1c114cb..259c145 100644 --- a/src/__tests__/actions.browser.test.ts +++ b/src/__tests__/actions.browser.test.ts @@ -50,7 +50,7 @@ describe('initSplitSdk', () => { // return of async action let action = store.getActions()[0]; expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); expect(onReadyCb.mock.calls.length).toBe(1); @@ -60,7 +60,7 @@ describe('initSplitSdk', () => { setTimeout(() => { action = store.getActions()[1]; expect(action.type).toEqual(SPLIT_UPDATE); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); expect(onUpdateCb.mock.calls.length).toBe(1); done(); @@ -80,7 +80,7 @@ describe('initSplitSdk', () => { // return of async action let action = store.getActions()[0]; expect(action.type).toEqual(SPLIT_TIMEDOUT); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); expect(onTimedoutCb.mock.calls.length).toBe(1); @@ -90,7 +90,7 @@ describe('initSplitSdk', () => { setTimeout(() => { action = store.getActions()[1]; expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); expect(onReadyCb.mock.calls.length).toBe(1); done(); @@ -106,7 +106,7 @@ describe('initSplitSdk', () => { // action should be already dispatched when the callback is called const action = store.getActions()[0]; expect(action.type).toEqual(SPLIT_READY_FROM_CACHE); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); }); const onReadyCb = jest.fn(() => { @@ -569,7 +569,7 @@ describe('destroySplitSdk', () => { actionResult.then(() => { const action = store.getActions()[3]; expect(action.type).toEqual(SPLIT_DESTROY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); // assert that all client's destroy methods were called expect(splitSdk.factory.client().destroy).toBeCalledTimes(1); diff --git a/src/__tests__/actions.node.test.ts b/src/__tests__/actions.node.test.ts index 3d99a73..df6ca28 100644 --- a/src/__tests__/actions.node.test.ts +++ b/src/__tests__/actions.node.test.ts @@ -54,7 +54,7 @@ describe('initSplitSdk', () => { // Action is dispatched synchronously const action = store.getActions()[0]; expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); } @@ -87,7 +87,7 @@ describe('initSplitSdk', () => { const action = store.getActions()[0]; expect(action.type).toEqual(SPLIT_TIMEDOUT); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); @@ -106,12 +106,12 @@ describe('initSplitSdk', () => { // Actions are dispatched synchronously const timeoutAction = store.getActions()[0]; expect(timeoutAction.type).toEqual(SPLIT_TIMEDOUT); - expect(timeoutAction.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(timeoutAction.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(timeoutAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp); const readyAction = store.getActions()[1]; expect(readyAction.type).toEqual(SPLIT_READY); - expect(readyAction.payload.timestamp).toBeLessThanOrEqual(Date.now()); + expect(readyAction.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); expect(readyAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp); } diff --git a/src/__tests__/reducer.test.ts b/src/__tests__/reducer.test.ts index bc4ec53..5653cc0 100644 --- a/src/__tests__/reducer.test.ts +++ b/src/__tests__/reducer.test.ts @@ -38,42 +38,42 @@ describe('Split reducer', () => { }); it('should handle SPLIT_READY', () => { - const readyAction = splitReady(); + const readyAction = splitReady(100); expect( splitReducer(initialState, readyAction), ).toEqual({ ...initialState, isReady: true, - lastUpdate: readyAction.payload.timestamp, + lastUpdate: 100, }); }); it('should handle SPLIT_READY_FROM_CACHE', () => { - const readyAction = splitReadyFromCache(); + const readyAction = splitReadyFromCache(200); expect( splitReducer(initialState, readyAction), ).toEqual({ ...initialState, isReadyFromCache: true, - lastUpdate: readyAction.payload.timestamp, + lastUpdate: 200, }); }); it('should handle SPLIT_TIMEDOUT', () => { - const timedoutAction = splitTimedout(); + const timedoutAction = splitTimedout(300); expect( splitReducer(initialState, timedoutAction), ).toEqual({ ...initialState, isTimedout: true, hasTimedout: true, - lastUpdate: timedoutAction.payload.timestamp, + lastUpdate: 300, }); }); it('should handle SPLIT_READY after SPLIT_TIMEDOUT', () => { - const timedoutAction = splitTimedout(); - const readyAction = splitReady(); + const timedoutAction = splitTimedout(100); + const readyAction = splitReady(200); expect( splitReducer(splitReducer(initialState, timedoutAction), readyAction), ).toEqual({ @@ -81,32 +81,32 @@ describe('Split reducer', () => { isReady: true, isTimedout: false, hasTimedout: true, - lastUpdate: readyAction.payload.timestamp, + lastUpdate: 200, }); }); it('should handle SPLIT_UPDATE', () => { - const updateAction = splitUpdate(); + const updateAction = splitUpdate(300); expect( splitReducer(initialState, updateAction), ).toEqual({ ...initialState, - lastUpdate: updateAction.payload.timestamp, + lastUpdate: 300, }); }); it('should handle SPLIT_DESTROY', () => { - const destroyAction = splitDestroy(); + const destroyAction = splitDestroy(400); expect( splitReducer(initialState, destroyAction), ).toEqual({ ...initialState, isDestroyed: true, - lastUpdate: destroyAction.payload.timestamp, + lastUpdate: 400, }); }); - const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig) => AnyAction, boolean, boolean]> = [ + const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) => AnyAction, boolean, boolean]> = [ ['ADD_TREATMENTS', addTreatments, false, false], ['SPLIT_READY_WITH_EVALUATIONS', splitReadyWithEvaluations, true, false], ['SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS', splitReadyFromCacheWithEvaluations, false, true], @@ -115,7 +115,7 @@ describe('Split reducer', () => { it.each(actionCreatorsWithEvaluations)('should handle %s', (_, actionCreator, isReady, isReadyFromCache) => { const initialTreatments = initialState.treatments; - const action = actionCreator(key, treatments); + const action = actionCreator(key, treatments, 1000); // control assertion - reduced state has the expected shape expect( @@ -124,7 +124,7 @@ describe('Split reducer', () => { ...initialState, isReady, isReadyFromCache, - lastUpdate: action.payload.timestamp || initialState.lastUpdate, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: treatments.test_split, @@ -142,7 +142,7 @@ describe('Split reducer', () => { const newTreatments: SplitIO.TreatmentsWithConfig = { test_split: { ...previousTreatment }, }; - const action = actionCreator(key, newTreatments); + const action = actionCreator(key, newTreatments, 1000); const reduxState = splitReducer(stateWithTreatments, action); // control assertion - treatment object was not replaced in the state @@ -155,7 +155,7 @@ describe('Split reducer', () => { ...initialState, isReady, isReadyFromCache, - lastUpdate: action.payload.timestamp || initialState.lastUpdate, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, @@ -173,7 +173,7 @@ describe('Split reducer', () => { config: previousTreatment.config, }, }; - const action = actionCreator(key, newTreatments); + const action = actionCreator(key, newTreatments, 1000); const reduxState = splitReducer(stateWithTreatments, action); // control assertion - treatment object was replaced in the state @@ -185,7 +185,7 @@ describe('Split reducer', () => { ...initialState, isReady, isReadyFromCache, - lastUpdate: action.payload.timestamp || initialState.lastUpdate, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, @@ -204,7 +204,7 @@ describe('Split reducer', () => { }, }; // const action = addTreatments(key, newTreatments); - const action = actionCreator(key, newTreatments); + const action = actionCreator(key, newTreatments, 1000); const reduxState = splitReducer(stateWithTreatments, action); // control assertion - treatment object was replaced in the state @@ -216,7 +216,7 @@ describe('Split reducer', () => { ...initialState, isReady, isReadyFromCache, - lastUpdate: action.payload.timestamp || initialState.lastUpdate, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, diff --git a/src/actions.ts b/src/actions.ts index 0d5b076..86223b5 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -8,80 +8,80 @@ import { } from './constants'; import { matching } from './utils'; -export function splitReady() { +export function splitReady(timestamp: number) { return { type: SPLIT_READY, payload: { - timestamp: Date.now(), + timestamp, }, }; } -export function splitReadyWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig) { +export function splitReadyWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { return { type: SPLIT_READY_WITH_EVALUATIONS, payload: { - timestamp: Date.now(), + timestamp, key: matching(key), treatments, }, }; } -export function splitReadyFromCache() { +export function splitReadyFromCache(timestamp: number) { return { type: SPLIT_READY_FROM_CACHE, payload: { - timestamp: Date.now(), + timestamp, }, }; } -export function splitReadyFromCacheWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig) { +export function splitReadyFromCacheWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { return { type: SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, payload: { - timestamp: Date.now(), + timestamp, key: matching(key), treatments, }, }; } -export function splitUpdate() { +export function splitUpdate(timestamp: number) { return { type: SPLIT_UPDATE, payload: { - timestamp: Date.now(), + timestamp, }, }; } -export function splitUpdateWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig) { +export function splitUpdateWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { return { type: SPLIT_UPDATE_WITH_EVALUATIONS, payload: { - timestamp: Date.now(), + timestamp, key: matching(key), treatments, }, }; } -export function splitTimedout() { +export function splitTimedout(timestamp: number) { return { type: SPLIT_TIMEDOUT, payload: { - timestamp: Date.now(), + timestamp, }, }; } -export function splitDestroy() { +export function splitDestroy(timestamp: number) { return { type: SPLIT_DESTROY, payload: { - timestamp: Date.now(), + timestamp, }, }; } diff --git a/src/asyncActions.ts b/src/asyncActions.ts index ba6651c..5e3f5a4 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -66,16 +66,16 @@ export function initSplitSdk(params: IInitSplitSdkParams): (dispatch: Dispatch): Promise => { dispatched = true; return Promise.all(destroyPromises).then(function () { - dispatch(splitDestroy()); + dispatch(splitDestroy(__getStatus(mainClient).lastUpdate)); if (params.onDestroy) params.onDestroy(); }); }; diff --git a/src/utils.ts b/src/utils.ts index 440e7e3..c7c6d6c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,8 +11,8 @@ export function isObject(obj: unknown) { /** * Verify type of key and return either its matchingKey or itself */ -export function matching(key: SplitIO.SplitKey): string { - return isObject(key) ? (key as SplitIO.SplitKeyObject).matchingKey : (key as string); +export function matching(key?: SplitIO.SplitKey): string | undefined { + return isObject(key) ? (key as SplitIO.SplitKeyObject).matchingKey : (key as string | undefined); } // The following utils might be removed in the future, if the JS SDK extends its public API with a "getStatus" method From 9e8667d269abd179a18e71819bd41c1df301bb0d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Sep 2024 18:16:20 -0300 Subject: [PATCH 05/25] Handle events of non-default clients --- src/__tests__/actions.browser.test.ts | 20 +++++++++++--------- src/actions.ts | 15 ++++++++++----- src/asyncActions.ts | 24 +++++++----------------- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/__tests__/actions.browser.test.ts b/src/__tests__/actions.browser.test.ts index 259c145..c0ffc3d 100644 --- a/src/__tests__/actions.browser.test.ts +++ b/src/__tests__/actions.browser.test.ts @@ -457,7 +457,7 @@ describe('getTreatments providing a user key', () => { }); }); - it('if Split SDK is ready but the user key is different than the main client, it stores control treatments (without calling SDK client), and registers pending evaluations to dispatch ADD_TREATMENTS actions when the new client is ready and updated', (done) => { + it('for non-default clients, it stores control treatments (without calling SDK client), and registers pending evaluations if the client is not operational, to dispatch it when ready from cache, ready, and updated (Using callbacks to assert that registered evaluations are not affected when the client timeouts)', (done) => { // Init SDK and set ready const store = mockStore(STATE_INITIAL); @@ -486,9 +486,9 @@ describe('getTreatments providing a user key', () => { (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_READY, 'other-user-key'); - // The ADD_TREATMENTS action is dispatched synchronously once the SDK is ready for the new user key + // The SPLIT_READY_WITH_EVALUATIONS action is dispatched synchronously once the SDK is ready for the new user key action = store.getActions()[2]; - expect(action.type).toBe(ADD_TREATMENTS); + expect(action.type).toBe(SPLIT_READY_WITH_EVALUATIONS); expect(action.payload.key).toBe('other-user-key'); // getting the evaluation result and validating it matches the results from SDK @@ -506,11 +506,10 @@ describe('getTreatments providing a user key', () => { expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); expect(Object.values(getClient(splitSdk, 'other-user-key').evalOnUpdate).length).toBe(1); // control assertion - added evalOnUpdate subscription - // The ADD_TREATMENTS action is dispatched when the SDK is updated - // SPLIT_UPDATE is not triggered since it is an update for a shared client + // The SPLIT_UPDATE_WITH_EVALUATIONS action is dispatched when the SDK is updated for the new user key (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE); action = store.getActions()[4]; - expect(action.type).toBe(ADD_TREATMENTS); + expect(action.type).toBe(SPLIT_UPDATE_WITH_EVALUATIONS); expect(action.payload.key).toBe('other-user-key'); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); @@ -524,10 +523,13 @@ describe('getTreatments providing a user key', () => { expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); expect(Object.values(getClient(splitSdk).evalOnUpdate).length).toBe(0); // control assertion - removed evalOnUpdate subscription - // Now, SDK_UPDATE events do not trigger ADD_TREATMENTS + // Now, SDK_UPDATE events do not trigger SPLIT_UPDATE_WITH_EVALUATIONS but SPLIT_UPDATE instead (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE); - expect(store.getActions().length).toBe(6); // control assertion - no more actions after the update. - expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - called 4 times + action = store.getActions()[6]; + expect(action.type).toBe(SPLIT_UPDATE); + + expect(store.getActions().length).toBe(7); // control assertion - no more actions after the update. + expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - called 4 times, in actions SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_READY_WITH_EVALUATIONS, SPLIT_UPDATE_WITH_EVALUATIONS and ADD_TREATMENTS. done(); }); diff --git a/src/actions.ts b/src/actions.ts index 86223b5..142efb3 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -8,11 +8,12 @@ import { } from './constants'; import { matching } from './utils'; -export function splitReady(timestamp: number) { +export function splitReady(timestamp: number, key?: SplitIO.SplitKey) { return { type: SPLIT_READY, payload: { timestamp, + key: matching(key), }, }; } @@ -28,11 +29,12 @@ export function splitReadyWithEvaluations(key: SplitIO.SplitKey, treatments: Spl }; } -export function splitReadyFromCache(timestamp: number) { +export function splitReadyFromCache(timestamp: number, key?: SplitIO.SplitKey) { return { type: SPLIT_READY_FROM_CACHE, payload: { timestamp, + key: matching(key), }, }; } @@ -48,11 +50,12 @@ export function splitReadyFromCacheWithEvaluations(key: SplitIO.SplitKey, treatm }; } -export function splitUpdate(timestamp: number) { +export function splitUpdate(timestamp: number, key?: SplitIO.SplitKey) { return { type: SPLIT_UPDATE, payload: { timestamp, + key: matching(key), }, }; } @@ -68,20 +71,22 @@ export function splitUpdateWithEvaluations(key: SplitIO.SplitKey, treatments: Sp }; } -export function splitTimedout(timestamp: number) { +export function splitTimedout(timestamp: number, key?: SplitIO.SplitKey) { return { type: SPLIT_TIMEDOUT, payload: { timestamp, + key: matching(key), }, }; } -export function splitDestroy(timestamp: number) { +export function splitDestroy(timestamp: number, key?: SplitIO.SplitKey) { return { type: SPLIT_DESTROY, payload: { timestamp, + key: matching(key), }, }; } diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 5e3f5a4..b175ec9 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -228,22 +228,18 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (!splitSdk.dispatch) return; const lastUpdate = __getStatus(client).lastUpdate; - // @TODO dispatch `splitReady` and `splitReadyWithEvaluations` for shared clients eventually if (client.evalOnReady.length) { const treatments = __getTreatments(client, client.evalOnReady); - if (!key) splitSdk.dispatch(splitReadyWithEvaluations((splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); - else splitSdk.dispatch(addTreatments(key, treatments)); - + splitSdk.dispatch(splitReadyWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); } else { - if (!key) splitSdk.dispatch(splitReady(lastUpdate)); + splitSdk.dispatch(splitReady(lastUpdate, key)); } }); // On SDK timed out, dispatch `splitTimedout` action client.once(client.Event.SDK_READY_TIMED_OUT, function onTimedout() { - // @TODO dispatch for shared clients eventually - if (splitSdk.dispatch && !key) splitSdk.dispatch(splitTimedout(__getStatus(client).lastUpdate)); + if (splitSdk.dispatch) splitSdk.dispatch(splitTimedout(__getStatus(client).lastUpdate, key)); }); // On SDK timed out, dispatch `splitReadyFromCache` action @@ -251,15 +247,12 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (!splitSdk.dispatch) return; const lastUpdate = __getStatus(client).lastUpdate; - // @TODO dispatch `splitReadyFromCache` and `splitReadyFromCacheWithEvaluations` for shared clients eventually if (client.evalOnReadyFromCache.length) { const treatments = __getTreatments(client, client.evalOnReadyFromCache); - if (!key) splitSdk.dispatch(splitReadyFromCacheWithEvaluations((splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); - else splitSdk.dispatch(addTreatments(key, treatments)); - + splitSdk.dispatch(splitReadyFromCacheWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); } else { - if (!key) splitSdk.dispatch(splitReadyFromCache(lastUpdate)); + splitSdk.dispatch(splitReadyFromCache(lastUpdate, key)); } }); @@ -268,16 +261,13 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (!splitSdk.dispatch) return; const lastUpdate = __getStatus(client).lastUpdate; - // @TODO dispatch `splitUpdate` and `splitUpdateWithEvaluations` for shared clients eventually const evalOnUpdate = Object.values(client.evalOnUpdate); if (evalOnUpdate.length) { const treatments = __getTreatments(client, evalOnUpdate); - if (!key) splitSdk.dispatch(splitUpdateWithEvaluations((splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); - else splitSdk.dispatch(addTreatments(key, treatments)); - + splitSdk.dispatch(splitUpdateWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); } else { - if (!key) splitSdk.dispatch(splitUpdate(lastUpdate)); + splitSdk.dispatch(splitUpdate(lastUpdate, key)); } }); From 94fd37a8e81d168f12f33380fb8ee23be82d97dc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Sep 2024 18:41:53 -0300 Subject: [PATCH 06/25] Update status property in state --- src/__tests__/reducer.test.ts | 47 ++++++++++++++++++++++--------- src/reducer.ts | 53 ++++++++++++++++++++++------------- src/types.ts | 6 ++++ 3 files changed, 73 insertions(+), 33 deletions(-) diff --git a/src/__tests__/reducer.test.ts b/src/__tests__/reducer.test.ts index 5653cc0..80e74f6 100644 --- a/src/__tests__/reducer.test.ts +++ b/src/__tests__/reducer.test.ts @@ -1,4 +1,4 @@ -import { splitReducer } from '../reducer'; +import { initialStatus, splitReducer } from '../reducer'; import { splitReady, splitReadyWithEvaluations, splitReadyFromCache, splitReadyFromCacheWithEvaluations, splitTimedout, splitUpdate, splitUpdateWithEvaluations, splitDestroy, addTreatments } from '../actions'; import { ISplitState } from '../types'; import SplitIO from '@splitsoftware/splitio/types/splitio'; @@ -12,6 +12,7 @@ const initialState: ISplitState = { isDestroyed: false, lastUpdate: 0, treatments: {}, + status: {} }; const key = 'userkey'; @@ -122,14 +123,19 @@ describe('Split reducer', () => { splitReducer(initialState, action), ).toEqual({ ...initialState, - isReady, - isReadyFromCache, - lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: treatments.test_split, }, }, + status: action.type === 'ADD_TREATMENTS' ? {} : { + [key]: { + ...initialStatus, + isReady, + isReadyFromCache, + lastUpdate: 1000, + } + } }); expect(initialState.treatments).toBe(initialTreatments); // control-assert initialState treatments object shouldn't be replaced @@ -153,14 +159,19 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, - isReady, - isReadyFromCache, - lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, + status: action.type === 'ADD_TREATMENTS' ? {} : { + [key]: { + ...initialStatus, + isReady, + isReadyFromCache, + lastUpdate: 1000, + } + } }); }); @@ -183,14 +194,19 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, - isReady, - isReadyFromCache, - lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, + status: action.type === 'ADD_TREATMENTS' ? {} : { + [key]: { + ...initialStatus, + isReady, + isReadyFromCache, + lastUpdate: 1000, + } + } }); }); @@ -214,14 +230,19 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, - isReady, - isReadyFromCache, - lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, + status: action.type === 'ADD_TREATMENTS' ? {} : { + [key]: { + ...initialStatus, + isReady, + isReadyFromCache, + lastUpdate: 1000, + } + } }); }); diff --git a/src/reducer.ts b/src/reducer.ts index 6778e07..1b719d3 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -20,49 +20,62 @@ export const initialStatus = { const initialState: ISplitState = { ...initialStatus, treatments: {}, + status: {}, }; -function setStatus(state: ISplitState, patch: Partial) { - return { +function setStatus(state: ISplitState, patch: Partial, key?: string) { + return key ? { + ...state, + status: { + ...state.status, + [key]: state.status[key] ? { + ...state.status[key], + ...patch, + } : { + ...initialStatus, + ...patch, + } + }, + } : { ...state, ...patch, }; } -function setReady(state: ISplitState, timestamp: number) { +function setReady(state: ISplitState, timestamp: number, key?: string) { return setStatus(state, { isReady: true, isTimedout: false, lastUpdate: timestamp, - }); + }, key); } -function setReadyFromCache(state: ISplitState, timestamp: number) { +function setReadyFromCache(state: ISplitState, timestamp: number, key?: string) { return setStatus(state, { isReadyFromCache: true, lastUpdate: timestamp, - }); + }, key); } -function setTimedout(state: ISplitState, timestamp: number) { +function setTimedout(state: ISplitState, timestamp: number, key?: string) { return setStatus(state, { isTimedout: true, hasTimedout: true, lastUpdate: timestamp, - }); + }, key); } -function setUpdated(state: ISplitState, timestamp: number) { +function setUpdated(state: ISplitState, timestamp: number, key?: string) { return setStatus(state, { lastUpdate: timestamp, - }); + }, key); } -function setDestroyed(state: ISplitState, timestamp: number) { +function setDestroyed(state: ISplitState, timestamp: number, key?: string) { return setStatus(state, { isDestroyed: true, lastUpdate: timestamp, - }); + }, key); } /** @@ -98,19 +111,19 @@ export const splitReducer: Reducer = function ( switch (type) { case SPLIT_READY: - return setReady(state, timestamp); + return setReady(state, timestamp, key); case SPLIT_READY_FROM_CACHE: - return setReadyFromCache(state, timestamp); + return setReadyFromCache(state, timestamp, key); case SPLIT_TIMEDOUT: - return setTimedout(state, timestamp); + return setTimedout(state, timestamp, key); case SPLIT_UPDATE: - return setUpdated(state, timestamp); + return setUpdated(state, timestamp, key); case SPLIT_DESTROY: - return setDestroyed(state, timestamp); + return setDestroyed(state, timestamp, key); case ADD_TREATMENTS: { const result = { ...state }; @@ -118,17 +131,17 @@ export const splitReducer: Reducer = function ( } case SPLIT_READY_WITH_EVALUATIONS: { - const result = setReady(state, timestamp); + const result = setReady(state, timestamp, key); return assignTreatments(result, key, treatments); } case SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS: { - const result = setReadyFromCache(state, timestamp); + const result = setReadyFromCache(state, timestamp, key); return assignTreatments(result, key, treatments); } case SPLIT_UPDATE_WITH_EVALUATIONS: { - const result = setUpdated(state, timestamp); + const result = setUpdated(state, timestamp, key); return assignTreatments(result, key, treatments); } diff --git a/src/types.ts b/src/types.ts index 60b0711..fccf8eb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,12 @@ export interface ISplitState extends IStatus { [key: string]: SplitIO.TreatmentWithConfig; }; }; + /** + * `status` is a nested object property that contains the readiness status of the non-default clients. + */ + status: { + [key: string]: IStatus; + }; } export type IGetSplitState = (state: any) => ISplitState; From a3fc06f1371ee9093ec0e6eae58e86404b89e128 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 14:42:30 -0300 Subject: [PATCH 07/25] Make status property optional, to avoid an interface breaking change --- CHANGES.txt | 1 + src/__tests__/reducer.test.ts | 9 ++++----- src/reducer.ts | 3 +-- src/types.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fc49236..59ba580 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 1.14.0 (September XX, 2024) + - Added `status` property to Split reducer's slice of state to track the readiness state of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. diff --git a/src/__tests__/reducer.test.ts b/src/__tests__/reducer.test.ts index 80e74f6..d684204 100644 --- a/src/__tests__/reducer.test.ts +++ b/src/__tests__/reducer.test.ts @@ -12,7 +12,6 @@ const initialState: ISplitState = { isDestroyed: false, lastUpdate: 0, treatments: {}, - status: {} }; const key = 'userkey'; @@ -128,7 +127,7 @@ describe('Split reducer', () => { [key]: treatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? {} : { + status: action.type === 'ADD_TREATMENTS' ? undefined : { [key]: { ...initialStatus, isReady, @@ -164,7 +163,7 @@ describe('Split reducer', () => { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? {} : { + status: action.type === 'ADD_TREATMENTS' ? undefined : { [key]: { ...initialStatus, isReady, @@ -199,7 +198,7 @@ describe('Split reducer', () => { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? {} : { + status: action.type === 'ADD_TREATMENTS' ? undefined : { [key]: { ...initialStatus, isReady, @@ -235,7 +234,7 @@ describe('Split reducer', () => { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? {} : { + status: action.type === 'ADD_TREATMENTS' ? undefined : { [key]: { ...initialStatus, isReady, diff --git a/src/reducer.ts b/src/reducer.ts index 1b719d3..8e162e2 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -20,7 +20,6 @@ export const initialStatus = { const initialState: ISplitState = { ...initialStatus, treatments: {}, - status: {}, }; function setStatus(state: ISplitState, patch: Partial, key?: string) { @@ -28,7 +27,7 @@ function setStatus(state: ISplitState, patch: Partial, key?: string) { ...state, status: { ...state.status, - [key]: state.status[key] ? { + [key]: state.status && state.status[key] ? { ...state.status[key], ...patch, } : { diff --git a/src/types.ts b/src/types.ts index fccf8eb..7819bff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,7 +58,7 @@ export interface ISplitState extends IStatus { /** * `status` is a nested object property that contains the readiness status of the non-default clients. */ - status: { + status?: { [key: string]: IStatus; }; } From e49acdd67dce1b3f1ee893a5a471a175abbc170a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 17:04:36 -0300 Subject: [PATCH 08/25] Update reducer unit tests --- src/__tests__/reducer.test.ts | 125 +++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/src/__tests__/reducer.test.ts b/src/__tests__/reducer.test.ts index d684204..358f56c 100644 --- a/src/__tests__/reducer.test.ts +++ b/src/__tests__/reducer.test.ts @@ -38,72 +38,145 @@ describe('Split reducer', () => { }); it('should handle SPLIT_READY', () => { - const readyAction = splitReady(100); - expect( - splitReducer(initialState, readyAction), - ).toEqual({ + const updatedState = splitReducer(initialState, splitReady(100)); + + // default key + expect(updatedState).toEqual({ ...initialState, isReady: true, lastUpdate: 100, }); + + // non-default key + expect(splitReducer(updatedState, splitReady(200, { matchingKey: 'other_key', bucketingKey: 'bucketing' }))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + isReady: true, + lastUpdate: 200, + } + } + }); }); it('should handle SPLIT_READY_FROM_CACHE', () => { - const readyAction = splitReadyFromCache(200); - expect( - splitReducer(initialState, readyAction), - ).toEqual({ - ...initialState, + const updatedState = splitReducer(initialState, splitReadyFromCache(200)); + + // default key + expect(updatedState).toEqual({ + ...updatedState, isReadyFromCache: true, lastUpdate: 200, }); + + // non-default key + expect(splitReducer(updatedState, splitReadyFromCache(300, 'other_key'))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + isReadyFromCache: true, + lastUpdate: 300, + } + } + }); }); it('should handle SPLIT_TIMEDOUT', () => { - const timedoutAction = splitTimedout(300); - expect( - splitReducer(initialState, timedoutAction), - ).toEqual({ + const updatedState = splitReducer(initialState, splitTimedout(300)); + + // default key + expect(updatedState).toEqual({ ...initialState, isTimedout: true, hasTimedout: true, lastUpdate: 300, }); + + // non-default key + expect(splitReducer(updatedState, splitTimedout(400, 'other_key'))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + isTimedout: true, + hasTimedout: true, + lastUpdate: 400, + } + } + }); }); it('should handle SPLIT_READY after SPLIT_TIMEDOUT', () => { - const timedoutAction = splitTimedout(100); - const readyAction = splitReady(200); - expect( - splitReducer(splitReducer(initialState, timedoutAction), readyAction), - ).toEqual({ + const updatedState = splitReducer(splitReducer(initialState, splitTimedout(100)), splitReady(200)); + + // default key + expect(updatedState).toEqual({ ...initialState, isReady: true, isTimedout: false, hasTimedout: true, lastUpdate: 200, }); + + // non-default key + expect(splitReducer(splitReducer(updatedState, splitTimedout(100, 'other_key')), splitReady(200, 'other_key'))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + isReady: true, + isTimedout: false, + hasTimedout: true, + lastUpdate: 200, + } + } + }); }); it('should handle SPLIT_UPDATE', () => { - const updateAction = splitUpdate(300); - expect( - splitReducer(initialState, updateAction), - ).toEqual({ + const updatedState = splitReducer(initialState, splitUpdate(300)); + + // default key + expect(updatedState).toEqual({ ...initialState, lastUpdate: 300, }); + + // non-default key + expect(splitReducer(updatedState, splitUpdate(400, 'other_key'))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + lastUpdate: 400, + } + } + }); }); it('should handle SPLIT_DESTROY', () => { - const destroyAction = splitDestroy(400); - expect( - splitReducer(initialState, destroyAction), - ).toEqual({ + const updatedState = splitReducer(initialState, splitDestroy(400)); + + // default key + expect(updatedState).toEqual({ ...initialState, isDestroyed: true, lastUpdate: 400, }); + + // non-default key + expect(splitReducer(updatedState, splitDestroy(500, 'other_key'))).toEqual({ + ...updatedState, + status: { + other_key: { + ...initialStatus, + isDestroyed: true, + lastUpdate: 500, + } + } + }); }); const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) => AnyAction, boolean, boolean]> = [ From 792927ddb66f6a00c9b6044cb6370c10cb386bc1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 17:35:31 -0300 Subject: [PATCH 09/25] Add custom Jest matcher to simplify unit tests --- jest.config.js | 5 ++- package-lock.json | 1 - package.json | 1 - src/__tests__/actions.browser.test.ts | 54 +++++++++++++++++--------- src/__tests__/actions.node.test.ts | 36 +++++++++++------ src/__tests__/utils/toBeWithinRange.ts | 36 +++++++++++++++++ 6 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 src/__tests__/utils/toBeWithinRange.ts diff --git a/jest.config.js b/jest.config.js index 8552e53..7fdf1b5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,5 +16,8 @@ module.exports = { collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/__tests__/**', - ] + ], + + // Custom jest matcher + setupFilesAfterEnv: ['/src/__tests__/utils/toBeWithinRange.ts'], }; diff --git a/package-lock.json b/package-lock.json index 7efe18b..4162196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", - "@types/jest": "^27.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.1", diff --git a/package.json b/package.json index c8ffe06..819e170 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", - "@types/jest": "^27.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.1", diff --git a/src/__tests__/actions.browser.test.ts b/src/__tests__/actions.browser.test.ts index 259c145..11f8d6c 100644 --- a/src/__tests__/actions.browser.test.ts +++ b/src/__tests__/actions.browser.test.ts @@ -49,9 +49,12 @@ describe('initSplitSdk', () => { actionResult.then(() => { // return of async action let action = store.getActions()[0]; - expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); expect(onReadyCb.mock.calls.length).toBe(1); @@ -59,9 +62,12 @@ describe('initSplitSdk', () => { (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_UPDATE); setTimeout(() => { action = store.getActions()[1]; - expect(action.type).toEqual(SPLIT_UPDATE); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_UPDATE, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); expect(onUpdateCb.mock.calls.length).toBe(1); done(); }, 0); @@ -79,9 +85,12 @@ describe('initSplitSdk', () => { actionResult.catch(() => { // return of async action let action = store.getActions()[0]; - expect(action.type).toEqual(SPLIT_TIMEDOUT); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_TIMEDOUT, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); expect(onTimedoutCb.mock.calls.length).toBe(1); @@ -89,9 +98,12 @@ describe('initSplitSdk', () => { (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); setTimeout(() => { action = store.getActions()[1]; - expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); expect(onReadyCb.mock.calls.length).toBe(1); done(); }, 0); @@ -105,9 +117,12 @@ describe('initSplitSdk', () => { const onReadyFromCacheCb = jest.fn(() => { // action should be already dispatched when the callback is called const action = store.getActions()[0]; - expect(action.type).toEqual(SPLIT_READY_FROM_CACHE); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_READY_FROM_CACHE, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); }); const onReadyCb = jest.fn(() => { const action = store.getActions()[1]; @@ -568,9 +583,12 @@ describe('destroySplitSdk', () => { actionResult.then(() => { const action = store.getActions()[3]; - expect(action.type).toEqual(SPLIT_DESTROY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_DESTROY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); // assert that all client's destroy methods were called expect(splitSdk.factory.client().destroy).toBeCalledTimes(1); expect(splitSdk.factory.client('other-user-key').destroy).toBeCalledTimes(1); diff --git a/src/__tests__/actions.node.test.ts b/src/__tests__/actions.node.test.ts index df6ca28..4d80498 100644 --- a/src/__tests__/actions.node.test.ts +++ b/src/__tests__/actions.node.test.ts @@ -53,9 +53,12 @@ describe('initSplitSdk', () => { // Action is dispatched synchronously const action = store.getActions()[0]; - expect(action.type).toEqual(SPLIT_READY); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now()), + } + }); } // create multiple stores @@ -86,9 +89,12 @@ describe('initSplitSdk', () => { store.dispatch(initSplitSdkAction); const action = store.getActions()[0]; - expect(action.type).toEqual(SPLIT_TIMEDOUT); - expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(action).toEqual({ + type: SPLIT_TIMEDOUT, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1) + } + }); expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1); timestamp = Date.now(); @@ -105,14 +111,20 @@ describe('initSplitSdk', () => { // Actions are dispatched synchronously const timeoutAction = store.getActions()[0]; - expect(timeoutAction.type).toEqual(SPLIT_TIMEDOUT); - expect(timeoutAction.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(timeoutAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(timeoutAction).toEqual({ + type: SPLIT_TIMEDOUT, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1) + } + }); const readyAction = store.getActions()[1]; - expect(readyAction.type).toEqual(SPLIT_READY); - expect(readyAction.payload.timestamp).toBeLessThanOrEqual(Date.now() + 1); - expect(readyAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp); + expect(readyAction).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1) + } + }); } // create multiple stores diff --git a/src/__tests__/utils/toBeWithinRange.ts b/src/__tests__/utils/toBeWithinRange.ts new file mode 100644 index 0000000..ebb5746 --- /dev/null +++ b/src/__tests__/utils/toBeWithinRange.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +// Custom matcher https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectextendmatchers +import { expect } from '@jest/globals'; + +expect.extend({ + toBeWithinRange(received: any, floor: number, ceiling: number) { + const pass = received >= floor && received <= ceiling; + if (pass) { + return { + message: () => + `expected ${received} not to be within range ${floor} - ${ceiling}`, + pass: true, + }; + } else { + return { + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + pass: false, + }; + } + }, +}); + +interface CustomMatchers { + toBeWithinRange(floor: number, ceiling: number): R; +} + +declare global { + namespace jest { + interface Expect extends CustomMatchers { } + interface Matchers extends CustomMatchers { } + interface InverseAsymmetricMatchers extends CustomMatchers { } + } +} From 8913f076f829f9942bdf3f19d5aa48752c463102 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 18:41:21 -0300 Subject: [PATCH 10/25] Rename test file to match src file --- .../{actions.browser.test.ts => asyncActions.browser.test.ts} | 0 src/__tests__/{actions.node.test.ts => asyncActions.node.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{actions.browser.test.ts => asyncActions.browser.test.ts} (100%) rename src/__tests__/{actions.node.test.ts => asyncActions.node.test.ts} (100%) diff --git a/src/__tests__/actions.browser.test.ts b/src/__tests__/asyncActions.browser.test.ts similarity index 100% rename from src/__tests__/actions.browser.test.ts rename to src/__tests__/asyncActions.browser.test.ts diff --git a/src/__tests__/actions.node.test.ts b/src/__tests__/asyncActions.node.test.ts similarity index 100% rename from src/__tests__/actions.node.test.ts rename to src/__tests__/asyncActions.node.test.ts From 22dc49f5726a804c5656ed88baa0af975d3ed7e9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 19:14:24 -0300 Subject: [PATCH 11/25] Unit tests: assert dispatched actions --- src/__tests__/asyncActions.browser.test.ts | 202 ++++++++++++++++----- src/__tests__/asyncActions.node.test.ts | 32 +++- 2 files changed, 185 insertions(+), 49 deletions(-) diff --git a/src/__tests__/asyncActions.browser.test.ts b/src/__tests__/asyncActions.browser.test.ts index 11f8d6c..ba29839 100644 --- a/src/__tests__/asyncActions.browser.test.ts +++ b/src/__tests__/asyncActions.browser.test.ts @@ -126,7 +126,12 @@ describe('initSplitSdk', () => { }); const onReadyCb = jest.fn(() => { const action = store.getActions()[1]; - expect(action.type).toEqual(SPLIT_READY); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1), + } + }); }); const actionResult = store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReady: onReadyCb, onReadyFromCache: onReadyFromCacheCb })); @@ -157,7 +162,7 @@ describe('initSplitSdk', () => { }); -describe('getTreatments', () => { +describe('getTreatments (for default user key)', () => { beforeEach(clearSplitSdk); @@ -202,12 +207,17 @@ describe('getTreatments', () => { actionResult.then(() => { store.dispatch(getTreatments({ splitNames: 'split1' })); - store.dispatch(getTreatments({ flagSets: 'set1' })); + store.dispatch(getTreatments({ flagSets: 'set1', key: { matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: 'bucket' } })); const actions = [store.getActions()[1], store.getActions()[2]]; actions.forEach(action => { - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object) + } + }); }); // getting the evaluation result and validating it matches the results from SDK @@ -232,21 +242,31 @@ describe('getTreatments', () => { function onReadyFromCacheCb() { // dispatching multiple ADD_TREATMENTS actions - store.dispatch(getTreatments({ splitNames: 'split1' })); // single feature flag name + store.dispatch(getTreatments({ splitNames: 'split1', key: sdkBrowserConfig.core.key })); // single feature flag name const attributes = { att1: 'att1' }; store.dispatch(getTreatments({ splitNames: ['split2', 'split3'], attributes })); // list of feature flag names with attributes // getting the 1st evaluation result and validating it matches the results from SDK let action = store.getActions()[1]; // action 0 is SPLIT_READY_FROM_CACHE - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenNthCalledWith(1, ['split1'], undefined); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveNthReturnedWith(1, action.payload.treatments); // getting the 2nd evaluation result and validating it matches the results from SDK action = store.getActions()[2]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenNthCalledWith(2, ['split2', 'split3'], attributes); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveNthReturnedWith(2, action.payload.treatments); expect(getClient(splitSdk).evalOnUpdate).toEqual({}); // control assertion - cbs scheduled for update @@ -257,8 +277,14 @@ describe('getTreatments', () => { actionResult.then(() => { // The SPLIT_READY_WITH_EVALUATIONS action is dispatched if the SDK is ready and there are pending evaluations. action = store.getActions()[3]; - expect(action.type).toBe(SPLIT_READY_WITH_EVALUATIONS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: SPLIT_READY_WITH_EVALUATIONS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object), + timestamp: expect.any(Number) + } + }); // Multiple evaluations where registered, but only one SPLIT_READY_WITH_EVALUATIONS action is dispatched expect(store.getActions().length).toBe(4); @@ -318,7 +344,12 @@ describe('getTreatments', () => { function onReadyFromCacheCb() { expect(store.getActions().length).toBe(3); const action = store.getActions()[2]; - expect(action.type).toBe(SPLIT_READY_FROM_CACHE); + expect(action).toEqual({ + type: SPLIT_READY_FROM_CACHE, + payload: { + timestamp: expect.any(Number) + } + }); } (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); @@ -326,8 +357,14 @@ describe('getTreatments', () => { actionResult.then(() => { // The SPLIT_READY_WITH_EVALUATIONS action is dispatched if the SDK is ready and there are pending evaluations. const action = store.getActions()[3]; - expect(action.type).toBe(SPLIT_READY_WITH_EVALUATIONS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: SPLIT_READY_WITH_EVALUATIONS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object), + timestamp: expect.any(Number) + } + }); // getting the evaluation result and validating it matches the results from SDK const treatments = action.payload.treatments; @@ -362,9 +399,13 @@ describe('getTreatments', () => { // If SDK is not ready, an ADD_TREATMENTS action is dispatched with control treatments without calling SDK client expect(store.getActions().length).toBe(1); let action = store.getActions()[0]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); - expect(action.payload.treatments).toEqual(getControlTreatmentsWithConfig(['split3'])); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: getControlTreatmentsWithConfig(['split3']) + } + }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toBeCalledTimes(0); // the item is added for evaluation on SDK_READY, and also on SDK_READY_FROM_CACHE and SDK_UPDATE events @@ -377,7 +418,12 @@ describe('getTreatments', () => { (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT); function onTimedoutCb() { action = store.getActions()[1]; - expect(action.type).toBe(SPLIT_TIMEDOUT); + expect(action).toEqual({ + type: SPLIT_TIMEDOUT, + payload: { + timestamp: expect.any(Number) + } + }); } // When the SDK is ready from cache, the SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS action is dispatched instead of @@ -385,8 +431,14 @@ describe('getTreatments', () => { (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); function onReadyFromCacheCb() { action = store.getActions()[2]; - expect(action.type).toBe(SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object), + timestamp: expect.any(Number) + } + }); // getting the evaluation result and validating it matches the results from SDK const treatments = action.payload.treatments; @@ -400,8 +452,14 @@ describe('getTreatments', () => { function onReadyCb() { // The SPLIT_READY_WITH_EVALUATIONS action is dispatched if the SDK is ready and there are pending evaluations. action = store.getActions()[3]; - expect(action.type).toBe(SPLIT_READY_WITH_EVALUATIONS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: SPLIT_READY_WITH_EVALUATIONS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object), + timestamp: expect.any(Number) + } + }); // getting the evaluation result and validating it matches the results from SDK let treatments = action.payload.treatments; @@ -413,8 +471,14 @@ describe('getTreatments', () => { // Triggering an update dispatches SPLIT_UPDATE_WITH_EVALUATIONS (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_UPDATE); action = store.getActions()[4]; - expect(action.type).toBe(SPLIT_UPDATE_WITH_EVALUATIONS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: SPLIT_UPDATE_WITH_EVALUATIONS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object), + timestamp: expect.any(Number) + } + }); // getting the evaluation result and validating it matches the results from SDK treatments = action.payload.treatments; @@ -424,15 +488,26 @@ describe('getTreatments', () => { expect(Object.values(getClient(splitSdk).evalOnUpdate).length).toBe(1); // control assertion - still have one evalOnUpdate subscription // We deregister the item from evalOnUpdate. - store.dispatch(getTreatments({ splitNames: 'split3', evalOnUpdate: false })); + store.dispatch(getTreatments({ splitNames: 'split3', evalOnUpdate: false, key: { matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: 'bucket' } })); action = store.getActions()[5]; - expect(action.type).toBe(ADD_TREATMENTS); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object) + } + }); expect(Object.values(getClient(splitSdk).evalOnUpdate).length).toBe(0); // control assertion - removed evalOnUpdate subscription // Now, SDK_UPDATE events do not trigger SPLIT_UPDATE_WITH_EVALUATIONS but SPLIT_UPDATE instead (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_UPDATE); action = store.getActions()[6]; - expect(action.type).toBe(SPLIT_UPDATE); + expect(action).toEqual({ + type: SPLIT_UPDATE, + payload: { + timestamp: expect.any(Number) + } + }); expect(store.getActions().length).toBe(7); // control assertion - no more actions after the update. expect(splitSdk.factory.client().getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - called 4 times, in actions SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_READY_WITH_EVALUATIONS, SPLIT_UPDATE_WITH_EVALUATIONS and ADD_TREATMENTS. @@ -443,7 +518,7 @@ describe('getTreatments', () => { }); -describe('getTreatments providing a user key', () => { +describe('getTreatments (providing a non-default user key)', () => { beforeEach(clearSplitSdk); @@ -462,8 +537,13 @@ describe('getTreatments providing a user key', () => { store.dispatch(getTreatments({ splitNames: 'split1', key: sdkBrowserConfig.core.key })); const action = store.getActions()[1]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(sdkBrowserConfig.core.key); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: sdkBrowserConfig.core.key, + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); expect(getClient(splitSdk).evalOnUpdate).toEqual({}); expect(getClient(splitSdk).evalOnReady.length).toEqual(0); @@ -472,7 +552,7 @@ describe('getTreatments providing a user key', () => { }); }); - it('if Split SDK is ready but the user key is different than the main client, it stores control treatments (without calling SDK client), and registers pending evaluations to dispatch ADD_TREATMENTS actions when the new client is ready and updated', (done) => { + it('for non-default clients, it stores control treatments (without calling SDK client), and registers pending evaluations if the client is not operational, to dispatch it when ready from cache, ready, and updated (Using callbacks to assert that registered evaluations are not affected when the client timeouts)', (done) => { // Init SDK and set ready const store = mockStore(STATE_INITIAL); @@ -483,7 +563,12 @@ describe('getTreatments providing a user key', () => { // SPLIT_READY should have been dispatched expect(store.getActions().length).toBe(1); let action = store.getActions()[0]; - expect(action.type).toBe(SPLIT_READY); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.any(Number) + } + }); // If SDK is ready for the main key and a getTreatment is dispatched for a different user key: // the item is added to the 'evalOnReady' list of the new client, @@ -494,17 +579,26 @@ describe('getTreatments providing a user key', () => { // and an ADD_TREATMENTS action is dispatched with control treatments without calling SDK client. action = store.getActions()[1]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe('other-user-key'); - expect(action.payload.treatments).toEqual(getControlTreatmentsWithConfig(['split2'])); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other-user-key', + treatments: getControlTreatmentsWithConfig(['split2']) + } + }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(0); (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_READY, 'other-user-key'); // The ADD_TREATMENTS action is dispatched synchronously once the SDK is ready for the new user key action = store.getActions()[2]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe('other-user-key'); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other-user-key', + treatments: expect.any(Object) + } + }); // getting the evaluation result and validating it matches the results from SDK const treatments = action.payload.treatments; @@ -517,7 +611,13 @@ describe('getTreatments providing a user key', () => { const attributes = { att1: 'att1' }; store.dispatch(getTreatments({ splitNames: 'split2', attributes, key: 'other-user-key', evalOnUpdate: true })); action = store.getActions()[3]; - expect(action.type).toBe(ADD_TREATMENTS); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other-user-key', + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); expect(Object.values(getClient(splitSdk, 'other-user-key').evalOnUpdate).length).toBe(1); // control assertion - added evalOnUpdate subscription @@ -525,8 +625,13 @@ describe('getTreatments providing a user key', () => { // SPLIT_UPDATE is not triggered since it is an update for a shared client (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE); action = store.getActions()[4]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe('other-user-key'); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other-user-key', + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); expect(Object.values(getClient(splitSdk, 'other-user-key').evalOnUpdate).length).toBe(1); // control assertion - keeping evalOnUpdate subscription @@ -534,7 +639,13 @@ describe('getTreatments providing a user key', () => { // We deregister the item from evalOnUpdate. store.dispatch(getTreatments({ splitNames: 'split2', key: 'other-user-key', evalOnUpdate: false })); action = store.getActions()[5]; - expect(action.type).toBe(ADD_TREATMENTS); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other-user-key', + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], undefined); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); expect(Object.values(getClient(splitSdk).evalOnUpdate).length).toBe(0); // control assertion - removed evalOnUpdate subscription @@ -611,7 +722,12 @@ describe('destroySplitSdk', () => { expect(splitSdk.factory.client().destroy).toBeCalledTimes(1); const action = store.getActions()[1]; - expect(action.type).toEqual(SPLIT_DESTROY); + expect(action).toEqual({ + type: SPLIT_DESTROY, + payload: { + timestamp: expect.any(Number), + } + }); done(); } }); diff --git a/src/__tests__/asyncActions.node.test.ts b/src/__tests__/asyncActions.node.test.ts index 4d80498..7bf6f0d 100644 --- a/src/__tests__/asyncActions.node.test.ts +++ b/src/__tests__/asyncActions.node.test.ts @@ -183,8 +183,13 @@ describe('getTreatments', () => { const actions = [store.getActions()[1], store.getActions()[2]]; // action 0 is SPLIT_READY actions.forEach(action => { - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe(splitKey); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: splitKey, + treatments: expect.any(Object) + } + }); }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenLastCalledWith(splitKey, ['split1'], undefined); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(actions[0].payload.treatments); @@ -197,8 +202,13 @@ describe('getTreatments', () => { store.dispatch(getTreatments({ key: 'other_user', splitNames: featureFlagNames, attributes })); const action = store.getActions()[3]; - expect(action.type).toBe(ADD_TREATMENTS); - expect(action.payload.key).toBe('other_user'); + expect(action).toEqual({ + type: ADD_TREATMENTS, + payload: { + key: 'other_user', + treatments: expect.any(Object) + } + }); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenLastCalledWith('other_user', featureFlagNames, attributes); expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); } @@ -253,9 +263,19 @@ describe('destroySplitSdk', () => { newStoreAfterDestroy.dispatch(initSplitSdkAction); let action = newStoreAfterDestroy.getActions()[0]; - expect(action.type).toEqual(SPLIT_READY); + expect(action).toEqual({ + type: SPLIT_READY, + payload: { + timestamp: expect.any(Number), + } + }); action = newStoreAfterDestroy.getActions()[1]; - expect(action.type).toEqual(SPLIT_DESTROY); + expect(action).toEqual({ + type: SPLIT_DESTROY, + payload: { + timestamp: expect.any(Number), + } + }); done(); } From 20ae830bb6d7054c9572b86b84bbb515f26beb09 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 10 Sep 2024 19:20:47 -0300 Subject: [PATCH 12/25] Fix test --- src/__tests__/asyncActions.browser.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/__tests__/asyncActions.browser.test.ts b/src/__tests__/asyncActions.browser.test.ts index 96decef..9b15acb 100644 --- a/src/__tests__/asyncActions.browser.test.ts +++ b/src/__tests__/asyncActions.browser.test.ts @@ -596,7 +596,8 @@ describe('getTreatments (providing a non-default user key)', () => { type: SPLIT_READY_WITH_EVALUATIONS, payload: { key: 'other-user-key', - treatments: expect.any(Object) + treatments: expect.any(Object), + timestamp: expect.any(Number) } }); @@ -628,7 +629,8 @@ describe('getTreatments (providing a non-default user key)', () => { type: SPLIT_UPDATE_WITH_EVALUATIONS, payload: { key: 'other-user-key', - treatments: expect.any(Object) + treatments: expect.any(Object), + timestamp: expect.any(Number) } }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); From 96eb7774cc3552a4ce8289540ab6ee7b9350b1b9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 11:37:28 -0300 Subject: [PATCH 13/25] Update SPLIT_READY_WITH_EVALUATIONS action to handle default and non-default clients --- src/__tests__/asyncActions.browser.test.ts | 14 ++++-- src/__tests__/reducer.test.ts | 54 ++++++++++------------ src/actions.ts | 9 ++-- src/asyncActions.ts | 6 +-- src/reducer.ts | 8 ++-- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/__tests__/asyncActions.browser.test.ts b/src/__tests__/asyncActions.browser.test.ts index 9b15acb..2345b17 100644 --- a/src/__tests__/asyncActions.browser.test.ts +++ b/src/__tests__/asyncActions.browser.test.ts @@ -597,7 +597,8 @@ describe('getTreatments (providing a non-default user key)', () => { payload: { key: 'other-user-key', treatments: expect.any(Object), - timestamp: expect.any(Number) + timestamp: expect.any(Number), + nonDefaultKey: true } }); @@ -630,7 +631,8 @@ describe('getTreatments (providing a non-default user key)', () => { payload: { key: 'other-user-key', treatments: expect.any(Object), - timestamp: expect.any(Number) + timestamp: expect.any(Number), + nonDefaultKey: true } }); expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], attributes); @@ -654,7 +656,13 @@ describe('getTreatments (providing a non-default user key)', () => { // Now, SDK_UPDATE events do not trigger SPLIT_UPDATE_WITH_EVALUATIONS but SPLIT_UPDATE instead (splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE); action = store.getActions()[6]; - expect(action.type).toBe(SPLIT_UPDATE); + expect(action).toEqual({ + type: SPLIT_UPDATE, + payload: { + key: 'other-user-key', + timestamp: expect.any(Number) + } + }); expect(store.getActions().length).toBe(7); // control assertion - no more actions after the update. expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - called 4 times, in actions SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_READY_WITH_EVALUATIONS, SPLIT_UPDATE_WITH_EVALUATIONS and ADD_TREATMENTS. diff --git a/src/__tests__/reducer.test.ts b/src/__tests__/reducer.test.ts index 358f56c..07fb1b6 100644 --- a/src/__tests__/reducer.test.ts +++ b/src/__tests__/reducer.test.ts @@ -179,7 +179,7 @@ describe('Split reducer', () => { }); }); - const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) => AnyAction, boolean, boolean]> = [ + const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number, nonDefaultKey?: boolean) => AnyAction, boolean, boolean]> = [ ['ADD_TREATMENTS', addTreatments, false, false], ['SPLIT_READY_WITH_EVALUATIONS', splitReadyWithEvaluations, true, false], ['SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS', splitReadyFromCacheWithEvaluations, false, true], @@ -188,12 +188,23 @@ describe('Split reducer', () => { it.each(actionCreatorsWithEvaluations)('should handle %s', (_, actionCreator, isReady, isReadyFromCache) => { const initialTreatments = initialState.treatments; + + // default key const action = actionCreator(key, treatments, 1000); + expect(splitReducer(initialState, action)).toEqual({ + ...initialState, + isReady, + isReadyFromCache, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, + treatments: { + test_split: { + [key]: treatments.test_split, + }, + }, + }); - // control assertion - reduced state has the expected shape - expect( - splitReducer(initialState, action), - ).toEqual({ + // non-default key + expect(splitReducer(initialState, actionCreator(key, treatments, 1000, true))).toEqual({ ...initialState, treatments: { test_split: { @@ -231,19 +242,14 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, + isReady, + isReadyFromCache, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? undefined : { - [key]: { - ...initialStatus, - isReady, - isReadyFromCache, - lastUpdate: 1000, - } - } }); }); @@ -266,19 +272,14 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, + isReady, + isReadyFromCache, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? undefined : { - [key]: { - ...initialStatus, - isReady, - isReadyFromCache, - lastUpdate: 1000, - } - } }); }); @@ -302,19 +303,14 @@ describe('Split reducer', () => { reduxState, ).toEqual({ ...initialState, + isReady, + isReadyFromCache, + lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000, treatments: { test_split: { [key]: newTreatments.test_split, }, }, - status: action.type === 'ADD_TREATMENTS' ? undefined : { - [key]: { - ...initialStatus, - isReady, - isReadyFromCache, - lastUpdate: 1000, - } - } }); }); diff --git a/src/actions.ts b/src/actions.ts index 142efb3..0c47857 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -18,13 +18,14 @@ export function splitReady(timestamp: number, key?: SplitIO.SplitKey) { }; } -export function splitReadyWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { +export function splitReadyWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number, nonDefaultKey?: boolean) { return { type: SPLIT_READY_WITH_EVALUATIONS, payload: { timestamp, key: matching(key), treatments, + nonDefaultKey, }, }; } @@ -39,13 +40,14 @@ export function splitReadyFromCache(timestamp: number, key?: SplitIO.SplitKey) { }; } -export function splitReadyFromCacheWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { +export function splitReadyFromCacheWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number, nonDefaultKey?: boolean) { return { type: SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, payload: { timestamp, key: matching(key), treatments, + nonDefaultKey, }, }; } @@ -60,13 +62,14 @@ export function splitUpdate(timestamp: number, key?: SplitIO.SplitKey) { }; } -export function splitUpdateWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) { +export function splitUpdateWithEvaluations(key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number, nonDefaultKey?: boolean) { return { type: SPLIT_UPDATE_WITH_EVALUATIONS, payload: { timestamp, key: matching(key), treatments, + nonDefaultKey, }, }; } diff --git a/src/asyncActions.ts b/src/asyncActions.ts index b175ec9..cc9027d 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -231,7 +231,7 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (client.evalOnReady.length) { const treatments = __getTreatments(client, client.evalOnReady); - splitSdk.dispatch(splitReadyWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); + splitSdk.dispatch(splitReadyWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate, key && true)); } else { splitSdk.dispatch(splitReady(lastUpdate, key)); } @@ -250,7 +250,7 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (client.evalOnReadyFromCache.length) { const treatments = __getTreatments(client, client.evalOnReadyFromCache); - splitSdk.dispatch(splitReadyFromCacheWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); + splitSdk.dispatch(splitReadyFromCacheWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate, key && true)); } else { splitSdk.dispatch(splitReadyFromCache(lastUpdate, key)); } @@ -265,7 +265,7 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN if (evalOnUpdate.length) { const treatments = __getTreatments(client, evalOnUpdate); - splitSdk.dispatch(splitUpdateWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate)); + splitSdk.dispatch(splitUpdateWithEvaluations(key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments, lastUpdate, key && true)); } else { splitSdk.dispatch(splitUpdate(lastUpdate, key)); } diff --git a/src/reducer.ts b/src/reducer.ts index 8e162e2..3e8e6d7 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -106,7 +106,7 @@ export const splitReducer: Reducer = function ( state = initialState, action, ) { - const { type, payload: { timestamp, key, treatments } = {} as any } = action as any; + const { type, payload: { timestamp, key, treatments, nonDefaultKey } = {} as any } = action as any; switch (type) { case SPLIT_READY: @@ -130,17 +130,17 @@ export const splitReducer: Reducer = function ( } case SPLIT_READY_WITH_EVALUATIONS: { - const result = setReady(state, timestamp, key); + const result = setReady(state, timestamp, nonDefaultKey && key); return assignTreatments(result, key, treatments); } case SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS: { - const result = setReadyFromCache(state, timestamp, key); + const result = setReadyFromCache(state, timestamp, nonDefaultKey && key); return assignTreatments(result, key, treatments); } case SPLIT_UPDATE_WITH_EVALUATIONS: { - const result = setUpdated(state, timestamp, key); + const result = setUpdated(state, timestamp, nonDefaultKey && key); return assignTreatments(result, key, treatments); } From 917fabfa6efb996142a8667beab4a7bad44a7e1e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 11:37:57 -0300 Subject: [PATCH 14/25] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4162196..077747d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.0", + "version": "1.13.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.13.0", + "version": "1.13.1-rc.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index 819e170..8ae4255 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.0", + "version": "1.13.1-rc.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", From 269c4e9108a6e7ae56d79a57b1b600cd7d8b5099 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 15:37:05 -0300 Subject: [PATCH 15/25] Create isMainClient function to reuse as a util --- src/asyncActions.ts | 7 +++---- src/helpers.ts | 5 ++--- src/utils.ts | 43 ++++++++++++++++++++++++------------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 5e3f5a4..6869802 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -4,7 +4,7 @@ import { Dispatch, Action } from 'redux'; import { IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ISplitFactoryBuilder } from './types'; import { splitReady, splitReadyWithEvaluations, splitReadyFromCache, splitReadyFromCacheWithEvaluations, splitTimedout, splitUpdate, splitUpdateWithEvaluations, splitDestroy, addTreatments } from './actions'; import { VERSION, ERROR_GETT_NO_INITSPLITSDK, ERROR_DESTROY_NO_INITSPLITSDK, getControlTreatmentsWithConfig } from './constants'; -import { matching, __getStatus, validateGetTreatmentsParams } from './utils'; +import { matching, __getStatus, validateGetTreatmentsParams, isMainClient } from './utils'; /** * Internal object SplitSdk. This object should not be accessed or @@ -209,13 +209,12 @@ interface IClientNotDetached extends SplitIO.IClient { export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientNotDetached { const stringKey = matching(key); - const isMainClient = !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); // we cannot simply use `stringKey` to get the client, since the main one could have been created with a bucketing key and/or a traffic type. - const client = (isMainClient ? splitSdk.factory.client() : splitSdk.factory.client(stringKey)) as IClientNotDetached; + const client = (isMainClient(key) ? splitSdk.factory.client() : splitSdk.factory.client(stringKey)) as IClientNotDetached; if (client._trackingStatus) return client; - if (!isMainClient) splitSdk.sharedClients[stringKey] = client; + if (!isMainClient(key)) splitSdk.sharedClients[stringKey] = client; client._trackingStatus = true; client.evalOnUpdate = {}; // getTreatment actions stored to execute on SDK update client.evalOnReady = []; // getTreatment actions stored to execute when the SDK is ready diff --git a/src/helpers.ts b/src/helpers.ts index bd89a12..73a12ee 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,7 +1,7 @@ import { splitSdk, getClient } from './asyncActions'; import { IStatus, ITrackParams } from './types'; import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK } from './constants'; -import { __getStatus, matching } from './utils'; +import { __getStatus, isMainClient, matching } from './utils'; import { initialStatus } from './reducer'; /** @@ -104,8 +104,7 @@ export function getSplits(): SplitIO.SplitViews { export function getStatus(key?: SplitIO.SplitKey): IStatus { if (splitSdk.factory) { const stringKey = matching(key); - const isMainClient = splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); - const client = isMainClient ? splitSdk.factory.client() : splitSdk.sharedClients[stringKey]; + const client = isMainClient(key) ? splitSdk.factory.client() : splitSdk.sharedClients[stringKey]; if (client) return __getStatus(client); } diff --git a/src/utils.ts b/src/utils.ts index c7c6d6c..0056ef4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,31 @@ +import { splitSdk } from './asyncActions'; import { ERROR_GETT_NO_PARAM_OBJECT, WARN_FEATUREFLAGS_AND_FLAGSETS } from './constants'; import { IGetTreatmentsParams } from './types'; /** - * Validates if a value is an object. + * Validates if a given value is a plain object */ export function isObject(obj: unknown) { return obj && typeof obj === 'object' && obj.constructor === Object; } +/** + * Validates if a given value is a string + */ +function isString(val: unknown): val is string { + return typeof val === 'string' || val instanceof String; +} + +/** + * Removes duplicate items on an array of strings + */ +function uniq(arr: string[]): string[] { + const seen: Record = {}; + return arr.filter((item) => { + return Object.prototype.hasOwnProperty.call(seen, item) ? false : seen[item] = true; + }); +} + /** * Verify type of key and return either its matchingKey or itself */ @@ -15,7 +33,10 @@ export function matching(key?: SplitIO.SplitKey): string | undefined { return isObject(key) ? (key as SplitIO.SplitKeyObject).matchingKey : (key as string | undefined); } -// The following utils might be removed in the future, if the JS SDK extends its public API with a "getStatus" method +export function isMainClient(key?: SplitIO.SplitKey) { + const stringKey = matching(key); + return splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); +} /** * ClientWithContext interface. @@ -30,6 +51,7 @@ export interface IClientStatus { lastUpdate: number; } +// The following util might be removed in the future, if the JS SDK extends its public API with a "getStatus" method export function __getStatus(client: SplitIO.IClient): IClientStatus { // @ts-expect-error, function exists but it is not part of JS SDK type definitions return client.__getStatus(); @@ -115,20 +137,3 @@ function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag nam return false; } - -/** - * Removes duplicate items on an array of strings. - */ -function uniq(arr: string[]): string[] { - const seen: Record = {}; - return arr.filter((item) => { - return Object.prototype.hasOwnProperty.call(seen, item) ? false : seen[item] = true; - }); -} - -/** - * Checks if a given value is a string. - */ -function isString(val: unknown): val is string { - return typeof val === 'string' || val instanceof String; -} From 8060bb7b5c39ca10c094c9c9936af52c1c6b9f18 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 16:20:38 -0300 Subject: [PATCH 16/25] Updated and selectors to retrieve status properties from the state rather than the SDK client instances directly --- CHANGES.txt | 3 +- src/__tests__/selectorsWithStatus.test.ts | 77 +++++++++++------------ src/__tests__/utils/storeState.ts | 16 +++++ src/selectors.ts | 18 +++++- src/utils.ts | 2 +- 5 files changed, 71 insertions(+), 45 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 59ba580..7e5e215 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ 1.14.0 (September XX, 2024) - - Added `status` property to Split reducer's slice of state to track the readiness state of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). + - Added `status` property to Split reducer's slice of state to track the SDK events of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). + - Updated `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors to retrieve status properties from the state rather than the SDK client instances directly. - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. - Updated some transitive dependencies for vulnerability fixes. diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index 2398b24..9ee844b 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -1,17 +1,13 @@ /** Mocks */ -import { SPLIT_1, SPLIT_2, STATE_READY, USER_1 } from './utils/storeState'; -import { mockSdk, Event } from './utils/mockBrowserSplitSdk'; +import { SPLIT_1, SPLIT_2, USER_1, USER_2, STATUS_INITIAL, STATE_INITIAL, STATE_READY, STATE_READY_USER_2 } from './utils/storeState'; +import { mockSdk } from './utils/mockBrowserSplitSdk'; jest.mock('@splitsoftware/splitio', () => { return { SplitFactory: mockSdk() }; }); - -import mockStore from './utils/mockStore'; -import { STATE_INITIAL, STATUS_INITIAL } from './utils/storeState'; -import { sdkBrowserConfig } from './utils/sdkConfigs'; -import { initSplitSdk, getTreatments, splitSdk } from '../asyncActions'; +import { initSplitSdk, splitSdk } from '../asyncActions'; /** Constants */ -import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE } from '../constants'; +import { ON, OFF, CONTROL, ERROR_SELECTOR_NO_SPLITSTATE } from '../constants'; /** Test targets */ import { @@ -44,54 +40,55 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => expect(errorSpy).not.toHaveBeenCalled(); }); - it('if getTreatments action was not dispatched for the provided feature flag and key, returns default treatment and client status', () => { - const store = mockStore(STATE_INITIAL); - store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); - (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + it('in client-side, there might be more than one client with its own status', () => { + initSplitSdk({ + config: { + core: { + authorizationKey: 'SDK KEY', + key: USER_1, + }, + } + }); - expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ - treatment: CONTROL, - // status of main client: - ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate, + // Main client is ready and has treatments + expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({ + treatment: ON, + ...STATUS_INITIAL, isReady: true, lastUpdate: STATE_READY.splitio.lastUpdate, }); - expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ - treatment: 'some_value', - // USER_1 client has not been initialized yet: - ...STATUS_INITIAL, + // USER_1 is the main client + expect(selectTreatmentWithConfigAndStatus(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ + treatment: { treatment: OFF, config: null }, + ...STATUS_INITIAL, isReady: true, lastUpdate: STATE_READY.splitio.lastUpdate, }); - store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); - (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); + // USER_2 client is not ready and has no treatments + expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1, USER_2)).toEqual({ + treatment: CONTROL, + ...STATUS_INITIAL, + }); - expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ - treatment: CONTROL_WITH_CONFIG, - // status of shared client: - ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate, + // USER_2 client is ready but has no treatments + expect(selectTreatmentAndStatus(STATE_READY_USER_2.splitio, SPLIT_2, USER_2)).toEqual({ + treatment: CONTROL, + ...STATUS_INITIAL, isReady: true, lastUpdate: STATE_READY_USER_2.splitio.status![USER_2].lastUpdate, }); expect(errorSpy).not.toHaveBeenCalled(); }); - it('happy path: returns the treatment value and status of the client', () => { - // The following actions result in STATE_READY state: - const store = mockStore(); - store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); - (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); - (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); - store.dispatch(getTreatments({ splitNames: [SPLIT_1] })); - store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); + it('in server-side, there is a single client and so all user keys share the same status', () => { + splitSdk.isDetached = true; expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({ treatment: ON, - ...STATUS_INITIAL, - isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate + ...STATUS_INITIAL, isReady: true, lastUpdate: STATE_READY.splitio.lastUpdate }); - expect(selectTreatmentWithConfigAndStatus(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ - treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], - ...STATUS_INITIAL, - isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate + // U + expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1, USER_2)).toEqual({ + treatment: CONTROL, + ...STATUS_INITIAL, isReady: true, lastUpdate: STATE_READY.splitio.lastUpdate }); expect(errorSpy).not.toHaveBeenCalled(); diff --git a/src/__tests__/utils/storeState.ts b/src/__tests__/utils/storeState.ts index 3cc9851..bbf1aa1 100644 --- a/src/__tests__/utils/storeState.ts +++ b/src/__tests__/utils/storeState.ts @@ -44,3 +44,19 @@ export const STATE_READY: { splitio: ISplitState } = { }, }, }; + +export const STATE_READY_USER_2: { splitio: ISplitState } = { + splitio: { + ...STATE_READY.splitio, + status: { + [USER_2]: { + isReady: true, + isReadyFromCache: false, + isTimedout: false, + hasTimedout: false, + isDestroyed: false, + lastUpdate: 1192838124, + }, + }, + }, +}; diff --git a/src/selectors.ts b/src/selectors.ts index c68064b..9b43ed0 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,7 +1,7 @@ import { ISplitState, IStatus } from './types'; import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; -import { matching } from './utils'; -import { getStatus } from './helpers'; +import { isMainClient, matching } from './utils'; +import { initialStatus } from './reducer'; export const getStateSlice = (sliceName: string) => (state: any) => state[sliceName]; @@ -76,10 +76,22 @@ export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, feat } & IStatus { const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); - const status = getStatus(key); + const status = selectStatus(splitState, key); return { ...status, treatment, }; } + +function selectStatus(splitState: ISplitState, key?: SplitIO.SplitKey): IStatus { + const status = splitState ? + isMainClient(key) ? + splitState : + splitState.status && splitState.status[matching(key)] : + console.error(ERROR_SELECTOR_NO_SPLITSTATE); + + return status ? + { isReady: status.isReady, isReadyFromCache: status.isReadyFromCache, isTimedout: status.isTimedout, hasTimedout: status.hasTimedout, isDestroyed: status.isDestroyed, lastUpdate: status.lastUpdate } : + { ...initialStatus }; +} diff --git a/src/utils.ts b/src/utils.ts index 0056ef4..3af94d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -35,7 +35,7 @@ export function matching(key?: SplitIO.SplitKey): string | undefined { export function isMainClient(key?: SplitIO.SplitKey) { const stringKey = matching(key); - return splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); + return splitSdk.isDetached || !stringKey || stringKey === matching(splitSdk.config && (splitSdk.config as SplitIO.IBrowserSettings).core.key); } /** From 722e293acb1de2cc3533696ee3221aa020def23f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 16:23:09 -0300 Subject: [PATCH 17/25] Fix typos --- src/__tests__/connectSplit.test.ts | 2 +- src/asyncActions.ts | 2 +- src/react-redux/connectSplit.ts | 2 +- src/utils.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__tests__/connectSplit.test.ts b/src/__tests__/connectSplit.test.ts index 65ef223..3515234 100644 --- a/src/__tests__/connectSplit.test.ts +++ b/src/__tests__/connectSplit.test.ts @@ -13,7 +13,7 @@ import { connectSplit } from '../react-redux/connectSplit'; const FeatureComponent = createComponentWithExposedProps('FeatureComponent'); describe('connectSplit', () => { - it('should pass the Split piece of state and binded getTreatment as props', () => { + it('should pass the Split piece of state and bound getTreatment as props', () => { const store = mockStore(STATE_READY); const ConnectedFeatureComponent: React.ComponentType = connectSplit()(FeatureComponent); diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 6869802..f0fc39a 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -16,7 +16,7 @@ export interface ISplitSdk { splitio: ISplitFactoryBuilder; factory: SplitIO.ISDK; sharedClients: { [stringKey: string]: SplitIO.IClient }; - isDetached: boolean; // true: server-side, false: client-side (i.e., client with binded key) + isDetached: boolean; // true: server-side, false: client-side (i.e., client with bound key) dispatch: Dispatch; } diff --git a/src/react-redux/connectSplit.ts b/src/react-redux/connectSplit.ts index dd1dec6..b0d48e1 100644 --- a/src/react-redux/connectSplit.ts +++ b/src/react-redux/connectSplit.ts @@ -6,7 +6,7 @@ import { defaultGetSplitState } from '../selectors'; /** * This decorator connects your components with: * - The Split state at Redux, under the prop key `split`. - * - The action creator `getTreatments`, binded to the `dispatch` of your store. + * - The action creator `getTreatments`, bound to the `dispatch` of your store. * * @param {IGetSplitState} getSplitState optional function that takes the entire Redux state and returns * the state slice which corresponds to where the Split reducer was mounted. This functionality is rarely diff --git a/src/utils.ts b/src/utils.ts index 0056ef4..3af94d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -35,7 +35,7 @@ export function matching(key?: SplitIO.SplitKey): string | undefined { export function isMainClient(key?: SplitIO.SplitKey) { const stringKey = matching(key); - return splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); + return splitSdk.isDetached || !stringKey || stringKey === matching(splitSdk.config && (splitSdk.config as SplitIO.IBrowserSettings).core.key); } /** From 470c3ec604195e09cb28fe9a2ca4a4dccd1ec3bf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Sep 2024 16:43:31 -0300 Subject: [PATCH 18/25] rc --- package-lock.json | 4 +- package.json | 2 +- src/__tests__/asyncActions.browser.test.ts | 55 +++++----------------- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 077747d..1f4dcc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.0", + "version": "1.13.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.0", + "version": "1.13.1-rc.1", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index 8ae4255..5c12668 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.0", + "version": "1.13.1-rc.1", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/__tests__/asyncActions.browser.test.ts b/src/__tests__/asyncActions.browser.test.ts index 2345b17..db2e172 100644 --- a/src/__tests__/asyncActions.browser.test.ts +++ b/src/__tests__/asyncActions.browser.test.ts @@ -162,7 +162,7 @@ describe('initSplitSdk', () => { }); -describe('getTreatments (for default user key)', () => { +describe('getTreatments', () => { beforeEach(clearSplitSdk); @@ -207,9 +207,10 @@ describe('getTreatments (for default user key)', () => { actionResult.then(() => { store.dispatch(getTreatments({ splitNames: 'split1' })); + store.dispatch(getTreatments({ splitNames: ['split2'], key: sdkBrowserConfig.core.key })); store.dispatch(getTreatments({ flagSets: 'set1', key: { matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: 'bucket' } })); - const actions = [store.getActions()[1], store.getActions()[2]]; + const actions = [store.getActions()[1], store.getActions()[2], store.getActions()[3]]; actions.forEach(action => { expect(action).toEqual({ type: ADD_TREATMENTS, @@ -221,10 +222,12 @@ describe('getTreatments (for default user key)', () => { }); // getting the evaluation result and validating it matches the results from SDK - expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split1'], undefined); - expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(actions[0].payload.treatments); + expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenCalledWith(['split1'], undefined); + expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveReturnedWith(actions[0].payload.treatments); + expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split2'], undefined); + expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(actions[1].payload.treatments); expect(splitSdk.factory.client().getTreatmentsWithConfigByFlagSets).toHaveBeenLastCalledWith(['set1'], undefined); - expect(splitSdk.factory.client().getTreatmentsWithConfigByFlagSets).toHaveLastReturnedWith(actions[1].payload.treatments); + expect(splitSdk.factory.client().getTreatmentsWithConfigByFlagSets).toHaveLastReturnedWith(actions[2].payload.treatments); expect(getClient(splitSdk).evalOnUpdate).toEqual({}); expect(getClient(splitSdk).evalOnReady.length).toEqual(0); @@ -516,43 +519,7 @@ describe('getTreatments (for default user key)', () => { } }); -}); - -describe('getTreatments (providing a non-default user key)', () => { - - beforeEach(clearSplitSdk); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('if Split SDK is ready and is provided with the same user key than the main client, it dispatches an ADD_TREATMENTS action as main client', (done) => { - - // Init SDK and set ready - const store = mockStore(STATE_INITIAL); - const actionResult = store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); - (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); - - actionResult.then(() => { - store.dispatch(getTreatments({ splitNames: 'split1', key: sdkBrowserConfig.core.key })); - - const action = store.getActions()[1]; - expect(action).toEqual({ - type: ADD_TREATMENTS, - payload: { - key: sdkBrowserConfig.core.key, - treatments: expect.any(Object) - } - }); - expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments); - expect(getClient(splitSdk).evalOnUpdate).toEqual({}); - expect(getClient(splitSdk).evalOnReady.length).toEqual(0); - - done(); - }); - }); - - it('for non-default clients, it stores control treatments (without calling SDK client), and registers pending evaluations if the client is not operational, to dispatch it when ready from cache, ready, and updated (Using callbacks to assert that registered evaluations are not affected when the client timeouts)', (done) => { + it('for non-default clients, it stores control treatments (without calling SDK client) and registers pending evaluations if the client is not operational, to dispatch it when ready from cache, ready, and updated (Using callbacks to assert that registered evaluations are not affected when the client timeouts)', (done) => { // Init SDK and set ready const store = mockStore(STATE_INITIAL); @@ -573,8 +540,8 @@ describe('getTreatments (providing a non-default user key)', () => { // If SDK is ready for the main key and a getTreatment is dispatched for a different user key: // the item is added to the 'evalOnReady' list of the new client, store.dispatch(getTreatments({ splitNames: 'split2', key: 'other-user-key' })); - expect(getClient(splitSdk).evalOnReady.length).toEqual(0); // control assertion - no evaluations were registeres for SDK_READY on main client - expect(getClient(splitSdk, 'other-user-key').evalOnReady.length).toEqual(1); // control assertion - 1 evaluation was registeres for SDK_READY on the new client + expect(getClient(splitSdk).evalOnReady.length).toEqual(0); // control assertion - no evaluations were registered for SDK_READY on main client + expect(getClient(splitSdk, 'other-user-key').evalOnReady.length).toEqual(1); // control assertion - 1 evaluation was registered for SDK_READY on the new client expect(getClient(splitSdk).evalOnUpdate).toEqual({}); // and an ADD_TREATMENTS action is dispatched with control treatments without calling SDK client. From b4348834fdc11568ef496c13e3b55ebc68797eef Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 13:31:46 -0300 Subject: [PATCH 19/25] Add selectStatus selector --- CHANGES.txt | 1 + src/index.ts | 2 +- src/selectors.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7e5e215..ac95a7e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ 1.14.0 (September XX, 2024) - Added `status` property to Split reducer's slice of state to track the SDK events of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). + - Added `selectStatus` selector to retrieve the status properties of the SDK manager and clients from the Split state. - Updated `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors to retrieve status properties from the state rather than the SDK client instances directly. - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. diff --git a/src/index.ts b/src/index.ts index c99eda8..6feb589 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export { splitReducer } from './reducer'; export { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from './asyncActions'; export { track, getSplitNames, getSplit, getSplits, getStatus } from './helpers'; -export { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from './selectors'; +export { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus, selectStatus } from './selectors'; // For React-redux export { connectSplit } from './react-redux/connectSplit'; diff --git a/src/selectors.ts b/src/selectors.ts index 9b43ed0..43ce15d 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -84,7 +84,14 @@ export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, feat }; } -function selectStatus(splitState: ISplitState, key?: SplitIO.SplitKey): IStatus { +/** + * This function extracts the status properties of the client for the given user key from the Split state. + * If no user key is provided, it returns the status properties of the main client, which matches the SDK manager status. + * + * @param {ISplitState} splitState + * @param {SplitIO.SplitKey} key + */ +export function selectStatus(splitState: ISplitState, key?: SplitIO.SplitKey): IStatus { const status = splitState ? isMainClient(key) ? splitState : From 54db7c2a2c36b3d02df6226e0a30cb21ca3a0a93 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 13:34:32 -0300 Subject: [PATCH 20/25] stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7e5e215..f5e3410 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.14.0 (September XX, 2024) +1.14.0 (September 12, 2024) - Added `status` property to Split reducer's slice of state to track the SDK events of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). - Updated `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors to retrieve status properties from the state rather than the SDK client instances directly. diff --git a/package-lock.json b/package-lock.json index 1f4dcc7..0df10ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.1", + "version": "1.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.1", + "version": "1.14.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index 5c12668..a1715c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.1", + "version": "1.14.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", From 2103250dff0ba18d64bec0b8363e0fd5ea98beed Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 15:36:09 -0300 Subject: [PATCH 21/25] Add ISplitAction type --- src/reducer.ts | 4 ++-- src/types.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/reducer.ts b/src/reducer.ts index 3e8e6d7..b220288 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -1,5 +1,5 @@ import { Reducer } from 'redux'; -import { ISplitState, IStatus } from './types'; +import { ISplitAction, ISplitState, IStatus } from './types'; import { SPLIT_READY, SPLIT_READY_WITH_EVALUATIONS, SPLIT_READY_FROM_CACHE, SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_UPDATE, SPLIT_UPDATE_WITH_EVALUATIONS, SPLIT_TIMEDOUT, SPLIT_DESTROY, ADD_TREATMENTS, @@ -106,7 +106,7 @@ export const splitReducer: Reducer = function ( state = initialState, action, ) { - const { type, payload: { timestamp, key, treatments, nonDefaultKey } = {} as any } = action as any; + const { type, payload: { timestamp, key, treatments, nonDefaultKey } = {} } = action as ISplitAction; switch (type) { case SPLIT_READY: diff --git a/src/types.ts b/src/types.ts index 7819bff..4a0909d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,3 +195,13 @@ export interface ITrackParams { } export type ISplitFactoryBuilder = (settings: SplitIO.IBrowserSettings | SplitIO.INodeSettings) => SplitIO.ISDK; + +export type ISplitAction = { + type: string; + payload: { + timestamp?: number; + key?: string; + treatments?: SplitIO.TreatmentsWithConfig; + nonDefaultKey?: boolean; + }; +} From bd8c52ffa82bb249beb7140545402abe5d40965f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 16:23:59 -0300 Subject: [PATCH 22/25] Update type definition comments --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/helpers.ts | 8 +++----- src/selectors.ts | 10 +++++++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5dd2d4e..5dca282 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.14.0 (September 12, 2024) +1.14.0 (September 13, 2024) - Added `status` property to Split reducer's slice of state to track the SDK events of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `selectStatus` selector to retrieve the status properties of the SDK manager and clients from the Split state. diff --git a/package-lock.json b/package-lock.json index 0df10ce..f1d0bdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.14.0", + "version": "1.13.1-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.14.0", + "version": "1.13.1-rc.2", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index a1715c3..22fc534 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.14.0", + "version": "1.13.1-rc.2", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/helpers.ts b/src/helpers.ts index 73a12ee..4b658ac 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -87,12 +87,10 @@ export function getSplits(): SplitIO.SplitViews { } /** - * Gets an object with the status properties of the SDK client or manager: + * Gets an object with the status properties of the SDK client or manager. * - * - `isReady` indicates if the SDK client has emitted the SDK_READY event. - * - `isReadyFromCache` indicates if the SDK client has emitted the SDK_READY_FROM_CACHE event. - * - `hasTimedout` indicates if the SDK client has emitted the SDK_READY_TIMED_OUT event. - * - `isDestroyed` indicates if the SDK client has been destroyed, i.e., if the `destroySplitSdk` action was dispatched. + * This function is similar to the `selectStatus` selector, but it does not require the Split state as a parameter since it uses the global `splitSdk` object. + * Consider using the `selectStatus` selector instead for a more Redux-friendly approach. * * @param {SplitIO.SplitKey} key To use only on client-side. Ignored in server-side. If a key is provided and a client associated to that key has been used, the status of that client is returned. * If no key is provided, the status of the main client and manager is returned (the main client shares the status with the manager). diff --git a/src/selectors.ts b/src/selectors.ts index 43ce15d..c678d35 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -85,11 +85,15 @@ export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, feat } /** - * This function extracts the status properties of the client for the given user key from the Split state. - * If no user key is provided, it returns the status properties of the main client, which matches the SDK manager status. + * Extracts an object with the status properties of the SDK manager or client from the Split state, for the given user key. * * @param {ISplitState} splitState - * @param {SplitIO.SplitKey} key + * @param {SplitIO.SplitKey} key To use only on client-side. Ignored in server-side. If a key is provided and a client associated to that key has been used, the status of that client is returned. + * If no key is provided, the status of the main client and manager is returned (the main client shares the status with the manager). + * + * @returns {IStatus} The status of the SDK client or manager. + * + * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#subscribe-to-events} */ export function selectStatus(splitState: ISplitState, key?: SplitIO.SplitKey): IStatus { const status = splitState ? From d728b47b0adef50cdc1267fff9c6aa6cccef4aa4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 16:45:17 -0300 Subject: [PATCH 23/25] Export remaining types --- CHANGES.txt | 1 + package-lock.json | 4 ++-- package.json | 2 +- src/index.ts | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5dca282..441f867 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,7 @@ - Added `status` property to Split reducer's slice of state to track the SDK events of non-default clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113). - Added `selectStatus` selector to retrieve the status properties of the SDK manager and clients from the Split state. + - Added remaining TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index in TypeScript, e.g., `import type { IInitSplitSdkParams } from '@splitsoftware/splitio-redux'`. - Updated `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors to retrieve status properties from the state rather than the SDK client instances directly. - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. diff --git a/package-lock.json b/package-lock.json index f1d0bdc..88845ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.2", + "version": "1.13.1-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.2", + "version": "1.13.1-rc.3", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index 22fc534..66b4f97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.2", + "version": "1.13.1-rc.3", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/index.ts b/src/index.ts index 6feb589..cf8060e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,5 @@ export { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStat export { connectSplit } from './react-redux/connectSplit'; export { connectToggler, mapTreatmentToProps, mapIsFeatureOnToProps } from './react-redux/connectToggler'; -export { ISplitState } from './types'; +// Types +export type { IStatus, ISplitState, IGetSplitState, IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ITrackParams } from './types'; From 0677eebe6afbb39bdd3c68c5f0fd98bdceac7dc2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 16:52:39 -0300 Subject: [PATCH 24/25] Fix some links --- package-lock.json | 4 ++-- package.json | 2 +- src/types.ts | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88845ce..0df10ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.3", + "version": "1.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.3", + "version": "1.14.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.28.0", diff --git a/package.json b/package.json index 66b4f97..a1715c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.13.1-rc.3", + "version": "1.14.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/types.ts b/src/types.ts index 4a0909d..4b2d6ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ export interface IStatus { /** * isReady indicates if Split client is ready, i.e., if it has emitted an SDK_READY event. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#subscribe-to-events} */ isReady: boolean; @@ -12,21 +12,21 @@ export interface IStatus { * isReadyFromCache indicates if Split client has emitted an SDK_READY_FROM_CACHE event, what means that the SDK is ready to * evaluate using LocalStorage cached data (which might be stale). * This flag only applies for the Browser if using LOCALSTORAGE as storage type. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#subscribe-to-events} */ isReadyFromCache: boolean; /** * isTimedout indicates if the Split client has emitted an SDK_READY_TIMED_OUT event and is not ready. * In other words, `isTimedout` is equivalent to `hasTimeout && !isReady`. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#subscribe-to-events} */ isTimedout: boolean; /** * hasTimedout indicates if the Split client has ever emitted an SDK_READY_TIMED_OUT event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#subscribe-to-events} */ hasTimedout: boolean; @@ -38,7 +38,7 @@ export interface IStatus { /** * lastUpdate is the timestamp of the last Split client event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). - * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} + * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#subscribe-to-events} */ lastUpdate: number; } From c7433a3e5c722605d09e3b64fdc1a3d2777e1aff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Sep 2024 17:05:51 -0300 Subject: [PATCH 25/25] Fix export --- src/index.ts | 2 +- src/selectors.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index cf8060e..0aefb73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,4 +9,4 @@ export { connectSplit } from './react-redux/connectSplit'; export { connectToggler, mapTreatmentToProps, mapIsFeatureOnToProps } from './react-redux/connectToggler'; // Types -export type { IStatus, ISplitState, IGetSplitState, IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ITrackParams } from './types'; +export { IStatus, ISplitState, IGetSplitState, IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ITrackParams } from './types'; diff --git a/src/selectors.ts b/src/selectors.ts index c678d35..ed1411e 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -85,7 +85,7 @@ export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, feat } /** - * Extracts an object with the status properties of the SDK manager or client from the Split state, for the given user key. + * Extracts an object with the status properties of the SDK client or manager from the Split state. * * @param {ISplitState} splitState * @param {SplitIO.SplitKey} key To use only on client-side. Ignored in server-side. If a key is provided and a client associated to that key has been used, the status of that client is returned.