diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 23cabcfe6..de672e1f3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,9 +1,9 @@ { - "packages/nest": "0.2.2", - "packages/react": "0.4.11", + "packages/nest": "0.2.3", + "packages/react": "1.0.0", "packages/angular": "0.0.1-experimental", - "packages/web": "1.4.1", - "packages/server": "1.17.1", - "packages/shared": "1.7.0", - "packages/angular/projects/angular-sdk": "0.0.9-experimental" + "packages/web": "1.5.0", + "packages/server": "1.18.0", + "packages/shared": "1.8.0", + "packages/angular/projects/angular-sdk": "0.0.12" } diff --git a/CODEOWNERS b/CODEOWNERS index d6e561d63..23c36d46e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,4 +3,4 @@ # # Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-javascript/workgroup.yaml # -* @open-feature/sdk-javascript-maintainers +* @open-feature/sdk-javascript-maintainers @open-feature/maintainers diff --git a/jest.config.ts b/jest.config.ts index 161e5d085..f37298f8c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -161,6 +161,7 @@ export default { testMatch: ['/packages/nest/test/**/*.spec.ts'], moduleNameMapper: { '@openfeature/core': '/packages/shared/src', + '@openfeature/server-sdk': '/packages/server/src', }, transform: { '^.+\\.ts$': [ diff --git a/package-lock.json b/package-lock.json index e6e41e9a1..6b7dc899a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@types/node": "^20.11.16", "@types/react": "^18.2.55", "@types/uuid": "^10.0.0", - "esbuild": "^0.24.0", + "esbuild": "^0.25.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", @@ -53,7 +53,7 @@ "tslib": "^2.3.0", "typedoc": "^0.26.0", "typescript": "^4.7.4", - "uuid": "^9.0.1" + "uuid": "^11.0.0" }, "engines": { "node": ">=18" @@ -79,12 +79,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.10.tgz", - "integrity": "sha512-/xudcHK2s4J/GcL6qyobmGaWMHQcYLSMqCaWMT+nK6I6tu9VEAj/p3R83Tzx8B/eKi31Pz499uHw9pmqdtbafg==", + "version": "0.1902.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.7.tgz", + "integrity": "sha512-XPKbesrGJ3qOHLcwb3y8X14NlBIwxnh9OvsfyqgBujByJq0LIg4CaU/GrX0Lo4RmX3UQBli668TjFgmIkMTL7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.10", + "@angular-devkit/core": "19.2.7", "rxjs": "7.8.1" }, "engines": { @@ -93,11 +94,12 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/core": { - "version": "18.2.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.10.tgz", - "integrity": "sha512-LFqiNdraBujg8e1lhuB0bkFVAoIbVbeXXwfoeROKH60OPbP8tHdgV6sFTqU7UGBKA+b+bYye70KFTG2Ys8QzKQ==", + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.7.tgz", + "integrity": "sha512-WeX/7HuNooJ4UhvVdremj6it0cX3nreG0/5r3QfrQd5Tz3sCHnh/lO5TW31gHtSqVgPjBGmzSzsyZ1Mi0lI7FA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", @@ -112,7 +114,7 @@ "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "chokidar": "^4.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -120,15 +122,50 @@ } } }, + "node_modules/@angular-devkit/architect/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.10.tgz", - "integrity": "sha512-EIm/yCYg3ZYPsPYJxXRX5F6PofJCbNQ5rZEuQEY09vy+ZRTqGezH0qoUP5WxlYeJrjiRLYqADI9WtVNzDyaD4w==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.7.tgz", + "integrity": "sha512-kE9W1MqfasumAYVD8egMHefyxmA93KfBYrWqcepZaFPQTPwg1AGTlID7YLHToLQquw4Iqen6Xv8Bzfv05IZ+tw==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.10", + "@angular-devkit/core": "19.2.7", "jsonc-parser": "3.3.1", - "magic-string": "0.30.11", + "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -138,37 +175,171 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.7.tgz", + "integrity": "sha512-WeX/7HuNooJ4UhvVdremj6it0cX3nreG0/5r3QfrQd5Tz3sCHnh/lO5TW31gHtSqVgPjBGmzSzsyZ1Mi0lI7FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-eslint/builder": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.3.tgz", - "integrity": "sha512-NzmrXlr7GFE+cjwipY/CxBscZXNqnuK0us1mO6Z2T6MeH6m+rRcdlY/rZyKoRniyNNvuzl6vpEsfMIMmnfebrA==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.3.0.tgz", + "integrity": "sha512-j9xNrzZJq29ONSG6EaeQHve0Squkm6u6Dm8fZgWP7crTFOrtLXn7Wxgxuyl9eddpbWY1Ov1gjFuwBVnxIdyAqg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": ">= 0.1800.0 < 0.1900.0", - "@angular-devkit/core": ">= 18.0.0 < 19.0.0" + "@angular-devkit/architect": ">= 0.1900.0 < 0.2000.0", + "@angular-devkit/core": ">= 19.0.0 < 20.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, + "node_modules/@angular-eslint/builder/node_modules/@angular-devkit/core": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.7.tgz", + "integrity": "sha512-WeX/7HuNooJ4UhvVdremj6it0cX3nreG0/5r3QfrQd5Tz3sCHnh/lO5TW31gHtSqVgPjBGmzSzsyZ1Mi0lI7FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/builder/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-eslint/builder/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.3.tgz", - "integrity": "sha512-zdrA8mR98X+U4YgHzUKmivRU+PxzwOL/j8G7eTOvBuq8GPzsP+hvak+tyxlgeGm9HsvpFj9ERHLtJ0xDUPs8fg==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.3.0.tgz", + "integrity": "sha512-63Zci4pvnUR1iSkikFlNbShF1tO5HOarYd8fvNfmOZwFfZ/1T3j3bCy9YbE+aM5SYrWqPaPP/OcwZ3wJ8WNvqA==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.4.3.tgz", - "integrity": "sha512-AyJbupiwTBR81P6T59v+aULEnPpZBCBxL2S5QFWfAhNCwWhcof4GihvdK2Z87yhvzDGeAzUFSWl/beJfeFa+PA==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.3.0.tgz", + "integrity": "sha512-nBLslLI20KnVbqlfNW7GcnI9R6cYCvRGjOE2QYhzxM316ciAQ62tvQuXP9ZVnRBLSKDAVnMeC0eTq9O4ysrxrQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.3", - "@angular-eslint/utils": "18.4.3" + "@angular-eslint/bundled-angular-compiler": "19.3.0", + "@angular-eslint/utils": "19.3.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -177,14 +348,14 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.4.3.tgz", - "integrity": "sha512-ijGlX2N01ayMXTpeQivOA31AszO8OEbu9ZQUCxnu9AyMMhxyi2q50bujRChAvN9YXQfdQtbxuajxV6+aiWb5BQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.3.0.tgz", + "integrity": "sha512-WyouppTpOYut+wvv13wlqqZ8EHoDrCZxNfGKuEUYK1BPmQlTB8EIZfQH4iR1rFVS28Rw+XRIiXo1x3oC0SOfnA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.3", - "@angular-eslint/utils": "18.4.3", + "@angular-eslint/bundled-angular-compiler": "19.3.0", + "@angular-eslint/utils": "19.3.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, @@ -206,39 +377,114 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.4.3.tgz", - "integrity": "sha512-D5maKn5e6n58+8n7jLFLD4g+RGPOPeDSsvPc1sqial5tEKLxAJQJS9WZ28oef3bhkob6C60D+1H0mMmEEVvyVA==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.3.0.tgz", + "integrity": "sha512-Wl5sFQ4t84LUb8mJ2iVfhYFhtF55IugXu7rRhPHtgIu9Ty5s1v3HGUx4LKv51m2kWhPPeFOTmjeBv1APzFlmnQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": ">= 18.0.0 < 19.0.0", - "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0", - "@angular-eslint/eslint-plugin": "18.4.3", - "@angular-eslint/eslint-plugin-template": "18.4.3", - "ignore": "6.0.2", - "semver": "7.6.3", + "@angular-devkit/core": ">= 19.0.0 < 20.0.0", + "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", + "@angular-eslint/eslint-plugin": "19.3.0", + "@angular-eslint/eslint-plugin-template": "19.3.0", + "ignore": "7.0.3", + "semver": "7.7.1", "strip-json-comments": "3.1.1" } }, + "node_modules/@angular-eslint/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.7.tgz", + "integrity": "sha512-WeX/7HuNooJ4UhvVdremj6it0cX3nreG0/5r3QfrQd5Tz3sCHnh/lO5TW31gHtSqVgPjBGmzSzsyZ1Mi0lI7FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-eslint/schematics/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/@angular-eslint/schematics/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@angular-eslint/template-parser": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.3.tgz", - "integrity": "sha512-JZMPtEB8yNip3kg4WDEWQyObSo2Hwf+opq2ElYuwe85GQkGhfJSJ2CQYo4FSwd+c5MUQAqESNRg9QqGYauDsiw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.3.0.tgz", + "integrity": "sha512-VxMNgsHXMWbbmZeBuBX5i8pzsSSEaoACVpaE+j8Muk60Am4Mxc0PytJm4n3znBSvI3B7Kq2+vStSRYPkOER4lA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.3", + "@angular-eslint/bundled-angular-compiler": "19.3.0", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -247,9 +493,9 @@ } }, "node_modules/@angular-eslint/template-parser/node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -264,13 +510,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "18.4.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.4.3.tgz", - "integrity": "sha512-w0bJ9+ELAEiPBSTPPm9bvDngfu1d8JbzUhvs2vU+z7sIz/HMwUZT5S4naypj2kNN0gZYGYrW0lt+HIbW87zTAQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.3.0.tgz", + "integrity": "sha512-ovvbQh96FIJfepHqLCMdKFkPXr3EbcvYc9kMj9hZyIxs/9/VxwPH7x25mMs4VsL6rXVgH2FgG5kR38UZlcTNNw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.4.3" + "@angular-eslint/bundled-angular-compiler": "19.3.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -2170,6 +2416,20 @@ "deprecated": "This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer.", "dev": true }, + "node_modules/@cucumber/messages/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.49.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", @@ -2185,9 +2445,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -2202,9 +2462,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -2219,9 +2479,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -2236,9 +2496,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -2253,9 +2513,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -2270,9 +2530,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -2287,9 +2547,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -2304,9 +2564,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -2321,9 +2581,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -2338,9 +2598,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -2355,9 +2615,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -2372,9 +2632,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -2389,9 +2649,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -2406,9 +2666,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -2423,9 +2683,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -2440,9 +2700,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -2457,9 +2717,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -2474,9 +2734,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -2491,9 +2751,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -2508,9 +2768,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -2525,9 +2785,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -2542,9 +2802,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -2559,9 +2819,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -2576,9 +2836,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -2593,9 +2853,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -9021,9 +9281,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9034,31 +9294,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/esbuild-wasm": { @@ -18441,16 +18701,17 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -19746,11 +20007,11 @@ "version": "0.0.0", "devDependencies": { "@angular-devkit/build-angular": "^19.0.0", - "@angular-eslint/builder": "18.4.3", - "@angular-eslint/eslint-plugin": "18.4.3", - "@angular-eslint/eslint-plugin-template": "18.4.3", - "@angular-eslint/schematics": "18.4.3", - "@angular-eslint/template-parser": "18.4.3", + "@angular-eslint/builder": "19.3.0", + "@angular-eslint/eslint-plugin": "19.3.0", + "@angular-eslint/eslint-plugin-template": "19.3.0", + "@angular-eslint/schematics": "19.3.0", + "@angular-eslint/template-parser": "19.3.0", "@angular/animations": "^19.0.0", "@angular/cli": "^19.0.0", "@angular/common": "^19.0.0", @@ -23322,6 +23583,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23723,7 +23985,7 @@ }, "packages/angular/projects/angular-sdk": { "name": "@openfeature/angular-sdk", - "version": "0.0.9-experimental", + "version": "0.0.11", "dependencies": { "tslib": "^2.3.0" }, @@ -23762,7 +24024,7 @@ }, "packages/react": { "name": "@openfeature/react-sdk", - "version": "0.4.10", + "version": "0.4.11", "license": "Apache-2.0", "devDependencies": { "@openfeature/core": "*", @@ -23789,7 +24051,7 @@ }, "packages/shared": { "name": "@openfeature/core", - "version": "1.7.0", + "version": "1.8.0", "license": "Apache-2.0", "devDependencies": {} }, diff --git a/package.json b/package.json index 320292dce..3104c1b41 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "description": "OpenFeature SDK for JavaScript", "scripts": { - "test": "jest --selectProjects=shared --selectProjects=server --selectProjects=web --selectProjects=react --selectProjects=angular --silent", + "test": "jest --selectProjects=shared --selectProjects=server --selectProjects=web --selectProjects=react --selectProjects=angular --selectProjects=nest --silent", "e2e-server": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/server/e2e/features && jest --selectProjects=server-e2e --verbose", "e2e-web": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/web/e2e/features && jest --selectProjects=web-e2e --verbose", "e2e": "npm run e2e-server && npm run e2e-web", @@ -42,7 +42,7 @@ "@types/node": "^20.11.16", "@types/react": "^18.2.55", "@types/uuid": "^10.0.0", - "esbuild": "^0.24.0", + "esbuild": "^0.25.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", @@ -70,7 +70,7 @@ "tslib": "^2.3.0", "typedoc": "^0.26.0", "typescript": "^4.7.4", - "uuid": "^9.0.1" + "uuid": "^11.0.0" }, "overrides": { "typescript": "^4.7.4" diff --git a/packages/angular/package.json b/packages/angular/package.json index 345666e13..66c36a164 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -15,11 +15,11 @@ "private": true, "devDependencies": { "@angular-devkit/build-angular": "^19.0.0", - "@angular-eslint/builder": "18.4.3", - "@angular-eslint/eslint-plugin": "18.4.3", - "@angular-eslint/eslint-plugin-template": "18.4.3", - "@angular-eslint/schematics": "18.4.3", - "@angular-eslint/template-parser": "18.4.3", + "@angular-eslint/builder": "19.3.0", + "@angular-eslint/eslint-plugin": "19.3.0", + "@angular-eslint/eslint-plugin-template": "19.3.0", + "@angular-eslint/schematics": "19.3.0", + "@angular-eslint/template-parser": "19.3.0", "@angular/animations": "^19.0.0", "@angular/cli": "^19.0.0", "@angular/common": "^19.0.0", diff --git a/packages/angular/projects/angular-sdk/CHANGELOG.md b/packages/angular/projects/angular-sdk/CHANGELOG.md index e2b3db07c..dccda7489 100644 --- a/packages/angular/projects/angular-sdk/CHANGELOG.md +++ b/packages/angular/projects/angular-sdk/CHANGELOG.md @@ -1,6 +1,30 @@ # Changelog +## [0.0.12](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.11...angular-sdk-v0.0.12) (2025-04-11) + + +### ✨ New Features + +* **angular:** add docs for setting evaluation context in angular ([#1170](https://github.com/open-feature/js-sdk/issues/1170)) ([24f1b23](https://github.com/open-feature/js-sdk/commit/24f1b230bf1d57971a336ac21b9ee46e8baf0cab)) + + +## [0.0.11](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.10...angular-sdk-v0.0.11) (2025-04-11) + + +### ✨ New Features + +* **angular:** add option for initial context injection ([aafdb43](https://github.com/open-feature/js-sdk/commit/aafdb4382f113f96a649f5fc0cecadb4178ada67)) + + +## [0.0.10](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.9-experimental...angular-sdk-v0.0.10) (2025-02-13) + + +### 🧹 Chore + +* **angular:** update angular package to a non-experimental version ([#1147](https://github.com/open-feature/js-sdk/issues/1147)) ([5272f76](https://github.com/open-feature/js-sdk/commit/5272f76c4075ebbd21f9b24dacac8f2d22e31ca9)), closes [#1110](https://github.com/open-feature/js-sdk/issues/1110) +* update sdk peer ([#1142](https://github.com/open-feature/js-sdk/issues/1142)) ([8bb6206](https://github.com/open-feature/js-sdk/commit/8bb620601e2b8dc7b62d717169b585bd1c886996)) + ## [0.0.9-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.8-experimental...angular-sdk-v0.0.9-experimental) (2024-11-21) diff --git a/packages/angular/projects/angular-sdk/README.md b/packages/angular/projects/angular-sdk/README.md index 129f21b40..a79b7efce 100644 --- a/packages/angular/projects/angular-sdk/README.md +++ b/packages/angular/projects/angular-sdk/README.md @@ -16,8 +16,8 @@ Specification - - Release + + Release
@@ -44,21 +44,22 @@ In addition to the features provided by the [web sdk](https://openfeature.dev/do - [Overview](#overview) - [Quick start](#quick-start) - - [Requirements](#requirements) - - [Install](#install) - - [npm](#npm) - - [yarn](#yarn) - - [Required peer dependencies](#required-peer-dependencies) - - [Usage](#usage) - - [Module](#module) - - [Minimal Example](#minimal-example) - - [How to use](#how-to-use) - - [Boolean Feature Flag](#boolean-feature-flag) - - [Number Feature Flag](#number-feature-flag) - - [String Feature Flag](#string-feature-flag) - - [Object Feature Flag](#object-feature-flag) - - [Opting-out of automatic re-rendering](#opting-out-of-automatic-re-rendering) - - [Consuming the evaluation details](#consuming-the-evaluation-details) + - [Requirements](#requirements) + - [Install](#install) + - [npm](#npm) + - [yarn](#yarn) + - [Required peer dependencies](#required-peer-dependencies) + - [Usage](#usage) + - [Module](#module) + - [Minimal Example](#minimal-example) + - [How to use](#how-to-use) + - [Boolean Feature Flag](#boolean-feature-flag) + - [Number Feature Flag](#number-feature-flag) + - [String Feature Flag](#string-feature-flag) + - [Object Feature Flag](#object-feature-flag) + - [Opting-out of automatic re-rendering](#opting-out-of-automatic-re-rendering) + - [Consuming the evaluation details](#consuming-the-evaluation-details) + - [Setting Evaluation Context](#setting-evaluation-context) - [FAQ and troubleshooting](#faq-and-troubleshooting) - [Resources](#resources) @@ -156,7 +157,7 @@ This parameter is optional, if omitted, the `thenTemplate` will always be render The `domain` parameter is _optional_ and will be used as domain when getting the OpenFeature provider. The `updateOnConfigurationChanged` and `updateOnContextChanged` parameter are _optional_ and used to disable the -automatic re-rendering on flag value or context change. They are set to `true` by default. +automatic re-rendering on flag value or contex change. They are set to `true` by default. The template referenced in `else` will be rendered if the evaluated feature flag is `false` for the `booleanFeatureFlag` directive and if the `value` does not match evaluated flag value for all other directives. @@ -281,6 +282,63 @@ This can be used to just render the flag value or details without conditional re ``` +##### Setting evaluation context + +To set the initial evaluation context, you can add the `context` parameter to the `OpenFeatureModule` configuration. +This context can be either an object or a factory function that returns an `EvaluationContext`. + +> [!TIP] +> Updating the context can be done directly via the global OpenFeature API using `OpenFeature.setContext()` + +Here’s how you can define and use the initial client evaluation context: + +###### Using a static object + +```typescript +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OpenFeatureModule } from '@openfeature/angular-sdk'; + +const initialContext = { + user: { + id: 'user123', + role: 'admin', + } +}; + +@NgModule({ + imports: [ + CommonModule, + OpenFeatureModule.forRoot({ + provider: yourFeatureProvider, + context: initialContext + }) + ], +}) +export class AppModule {} +``` + +###### Using a factory function + +```typescript +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OpenFeatureModule, EvaluationContext } from '@openfeature/angular-sdk'; + +const contextFactory = (): EvaluationContext => loadContextFromLocalStorage(); + +@NgModule({ + imports: [ + CommonModule, + OpenFeatureModule.forRoot({ + provider: yourFeatureProvider, + context: contextFactory + }) + ], +}) +export class AppModule {} +``` + ## FAQ and troubleshooting > I can import things form the `@openfeature/angular-sdk`, `@openfeature/web-sdk`, and `@openfeature/core`; which should I use? @@ -291,4 +349,4 @@ Avoid importing anything from `@openfeature/web-sdk` or `@openfeature/core`. ## Resources - - [Example repo](https://github.com/open-feature/angular-test-app) +- [Example repo](https://github.com/open-feature/angular-test-app) diff --git a/packages/angular/projects/angular-sdk/package.json b/packages/angular/projects/angular-sdk/package.json index f3a3e6c5d..f874abe88 100644 --- a/packages/angular/projects/angular-sdk/package.json +++ b/packages/angular/projects/angular-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/angular-sdk", - "version": "0.0.9-experimental", + "version": "0.0.12", "description": "OpenFeature Angular SDK", "repository": { "type": "git", diff --git a/packages/angular/projects/angular-sdk/src/lib/open-feature.module.ts b/packages/angular/projects/angular-sdk/src/lib/open-feature.module.ts index 6abc81d56..e667b6e8b 100644 --- a/packages/angular/projects/angular-sdk/src/lib/open-feature.module.ts +++ b/packages/angular/projects/angular-sdk/src/lib/open-feature.module.ts @@ -1,10 +1,13 @@ import { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { OpenFeature, Provider } from '@openfeature/web-sdk'; +import { EvaluationContext, OpenFeature, Provider } from '@openfeature/web-sdk'; + +export type EvaluationContextFactory = () => EvaluationContext; export interface OpenFeatureConfig { provider: Provider; domainBoundProviders?: Record; + context?: EvaluationContext | EvaluationContextFactory; } export const OPEN_FEATURE_CONFIG_TOKEN = new InjectionToken('OPEN_FEATURE_CONFIG_TOKEN'); @@ -16,7 +19,9 @@ export const OPEN_FEATURE_CONFIG_TOKEN = new InjectionToken(' }) export class OpenFeatureModule { static forRoot(config: OpenFeatureConfig): ModuleWithProviders { - OpenFeature.setProvider(config.provider); + const context = typeof config.context === 'function' ? config.context() : config.context; + OpenFeature.setProvider(config.provider, context); + if (config.domainBoundProviders) { Object.entries(config.domainBoundProviders).map(([domain, provider]) => OpenFeature.setProvider(domain, provider), diff --git a/packages/nest/CHANGELOG.md b/packages/nest/CHANGELOG.md index fba3e44e8..8ba7a7d54 100644 --- a/packages/nest/CHANGELOG.md +++ b/packages/nest/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.2.3](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.2...nestjs-sdk-v0.2.3) (2025-04-11) + + +### 🧹 Chore + +* update sdk peer ([#1142](https://github.com/open-feature/js-sdk/issues/1142)) ([8bb6206](https://github.com/open-feature/js-sdk/commit/8bb620601e2b8dc7b62d717169b585bd1c886996)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @openfeature/server-sdk bumped from * to 1.18.0 + ## [0.2.2](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.1-experimental...nestjs-sdk-v0.2.2) (2024-10-29) diff --git a/packages/nest/README.md b/packages/nest/README.md index 997444f0b..67a412da9 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -16,8 +16,8 @@ Specification - - Release + + Release
diff --git a/packages/nest/package.json b/packages/nest/package.json index 579191cf0..558996ef4 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/nestjs-sdk", - "version": "0.2.2", + "version": "0.2.3", "description": "OpenFeature Nest.js SDK", "main": "./dist/cjs/index.js", "files": [ @@ -57,7 +57,7 @@ "@nestjs/platform-express": "^10.3.6", "@nestjs/testing": "^10.3.6", "@openfeature/core": "*", - "@openfeature/server-sdk": "*", + "@openfeature/server-sdk": "1.18.0", "@types/supertest": "^6.0.0", "supertest": "^7.0.0" } diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 1b1d08299..9eebb994b 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.0.0](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.11...react-sdk-v1.0.0) (2025-04-14) + + +### ✨ New Features + +* add polyfill for react use hook ([#1157](https://github.com/open-feature/js-sdk/issues/1157)) ([5afe61f](https://github.com/open-feature/js-sdk/commit/5afe61f9e351b037b04c93a1d81aee8016756748)) +* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038)) + ## [0.4.11](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.10...react-sdk-v0.4.11) (2025-02-07) diff --git a/packages/react/README.md b/packages/react/README.md index a6226f597..c5f326407 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -16,8 +16,8 @@ Specification - - Release + + Release
diff --git a/packages/react/package.json b/packages/react/package.json index e6f5cbb64..fa8380b91 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/react-sdk", - "version": "0.4.11", + "version": "1.0.0", "description": "OpenFeature React SDK", "main": "./dist/cjs/index.js", "files": [ @@ -47,7 +47,7 @@ }, "homepage": "https://github.com/open-feature/js-sdk#readme", "peerDependencies": { - "@openfeature/web-sdk": "^1.4.1", + "@openfeature/web-sdk": "^1.5.0", "react": ">=16.8.0" }, "devDependencies": { diff --git a/packages/react/src/evaluation/use-feature-flag.ts b/packages/react/src/evaluation/use-feature-flag.ts index 899c042cc..c128dfaaf 100644 --- a/packages/react/src/evaluation/use-feature-flag.ts +++ b/packages/react/src/evaluation/use-feature-flag.ts @@ -5,18 +5,24 @@ import type { EventHandler, FlagEvaluationOptions, FlagValue, - JsonValue} from '@openfeature/web-sdk'; -import { - ProviderEvents, - ProviderStatus, + JsonValue, } from '@openfeature/web-sdk'; +import { ProviderEvents, ProviderStatus } from '@openfeature/web-sdk'; import { useEffect, useRef, useState } from 'react'; +import { + DEFAULT_OPTIONS, + isEqual, + normalizeOptions, + suspendUntilInitialized, + suspendUntilReconciled, + useProviderOptions, +} from '../internal'; import type { ReactFlagEvaluationNoSuspenseOptions, ReactFlagEvaluationOptions } from '../options'; -import { DEFAULT_OPTIONS, isEqual, normalizeOptions, suspendUntilReady, useProviderOptions } from '../internal'; import { useOpenFeatureClient } from '../provider/use-open-feature-client'; import { useOpenFeatureClientStatus } from '../provider/use-open-feature-client-status'; +import { useOpenFeatureProvider } from '../provider/use-open-feature-provider'; import type { FlagQuery } from '../query'; -import { HookFlagQuery } from './hook-flag-query'; +import { HookFlagQuery } from '../internal/hook-flag-query'; // This type is a bit wild-looking, but I think we need it. // We have to use the conditional, because otherwise useFlag('key', false) would return false, not boolean (too constrained). @@ -280,14 +286,16 @@ function attachHandlersAndResolve( const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options) }; const client = useOpenFeatureClient(); const status = useOpenFeatureClientStatus(); + const provider = useOpenFeatureProvider(); + + const controller = new AbortController(); - // suspense if (defaultedOptions.suspendUntilReady && status === ProviderStatus.NOT_READY) { - suspendUntilReady(client); + suspendUntilInitialized(provider, client); } if (defaultedOptions.suspendWhileReconciling && status === ProviderStatus.RECONCILING) { - suspendUntilReady(client); + suspendUntilReconciled(client); } const [evaluationDetails, setEvaluationDetails] = useState>( @@ -322,28 +330,23 @@ function attachHandlersAndResolve( useEffect(() => { if (status === ProviderStatus.NOT_READY) { // update when the provider is ready - client.addHandler(ProviderEvents.Ready, updateEvaluationDetailsCallback); + client.addHandler(ProviderEvents.Ready, updateEvaluationDetailsCallback, { signal: controller.signal }); } if (defaultedOptions.updateOnContextChanged) { // update when the context changes - client.addHandler(ProviderEvents.ContextChanged, updateEvaluationDetailsCallback); + client.addHandler(ProviderEvents.ContextChanged, updateEvaluationDetailsCallback, { signal: controller.signal }); } - return () => { - // cleanup the handlers - client.removeHandler(ProviderEvents.Ready, updateEvaluationDetailsCallback); - client.removeHandler(ProviderEvents.ContextChanged, updateEvaluationDetailsCallback); - }; - }, []); - useEffect(() => { if (defaultedOptions.updateOnConfigurationChanged) { // update when the provider configuration changes - client.addHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback); + client.addHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback, { + signal: controller.signal, + }); } return () => { // cleanup the handlers - client.removeHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback); + controller.abort(); }; }, []); diff --git a/packages/react/src/internal/context.ts b/packages/react/src/internal/context.ts index 4b495bac3..3fc7d7707 100644 --- a/packages/react/src/internal/context.ts +++ b/packages/react/src/internal/context.ts @@ -5,7 +5,8 @@ import { normalizeOptions } from '.'; /** * The underlying React context. - * DO NOT EXPORT PUBLICLY + * + * **DO NOT EXPORT PUBLICLY** * @internal */ export const Context = React.createContext< @@ -14,7 +15,8 @@ export const Context = React.createContext< /** * Get a normalized copy of the options used for this OpenFeatureProvider, see {@link normalizeOptions}. - * DO NOT EXPORT PUBLICLY + * + * **DO NOT EXPORT PUBLICLY** * @internal * @returns {NormalizedOptions} normalized options the defaulted options, not defaulted or normalized. */ diff --git a/packages/react/src/internal/errors.ts b/packages/react/src/internal/errors.ts new file mode 100644 index 000000000..81a72de65 --- /dev/null +++ b/packages/react/src/internal/errors.ts @@ -0,0 +1,9 @@ +const context = 'Components using OpenFeature must be wrapped with an .'; +const tip = 'If you are seeing this in a test, see: https://openfeature.dev/docs/reference/technologies/client/web/react#testing'; + +export class MissingContextError extends Error { + constructor(reason: string) { + super(`${reason}: ${context} ${tip}`); + this.name = 'MissingContextError'; + } +} \ No newline at end of file diff --git a/packages/react/src/evaluation/hook-flag-query.ts b/packages/react/src/internal/hook-flag-query.ts similarity index 100% rename from packages/react/src/evaluation/hook-flag-query.ts rename to packages/react/src/internal/hook-flag-query.ts diff --git a/packages/react/src/internal/suspense.ts b/packages/react/src/internal/suspense.ts index 72f4ca0d0..319a256e1 100644 --- a/packages/react/src/internal/suspense.ts +++ b/packages/react/src/internal/suspense.ts @@ -1,21 +1,56 @@ -import type { Client} from '@openfeature/web-sdk'; -import { ProviderEvents } from '@openfeature/web-sdk'; +import type { Client, Provider } from '@openfeature/web-sdk'; +import { NOOP_PROVIDER, ProviderEvents } from '@openfeature/web-sdk'; +import { use } from './use'; + +/** + * A weak map is used to store the global suspense status for each provider. It's + * important for this to be global to avoid rerender loops. Using useRef won't + * work because the value isn't preserved when a promise is thrown in a component, + * which is how suspense operates. + */ +const globalProviderSuspenseStatus = new WeakMap>(); /** * Suspends until the client is ready to evaluate feature flags. - * DO NOT EXPORT PUBLICLY - * @param {Client} client OpenFeature client + * + * **DO NOT EXPORT PUBLICLY** + * @internal + * @param {Provider} provider the provider to suspend for + * @param {Client} client the client to check for readiness */ -export function suspendUntilReady(client: Client): Promise { - let resolve: (value: unknown) => void; - let reject: () => void; - throw new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - client.addHandler(ProviderEvents.Ready, resolve); - client.addHandler(ProviderEvents.Error, reject); - }).finally(() => { - client.removeHandler(ProviderEvents.Ready, resolve); - client.removeHandler(ProviderEvents.Ready, reject); - }); +export function suspendUntilInitialized(provider: Provider, client: Client) { + const statusPromiseRef = globalProviderSuspenseStatus.get(provider); + if (!statusPromiseRef) { + // Noop provider is never ready, so we resolve immediately + const statusPromise = provider !== NOOP_PROVIDER ? isProviderReady(client) : Promise.resolve(); + globalProviderSuspenseStatus.set(provider, statusPromise); + // Use will throw the promise and React will trigger a rerender when it's resolved + use(statusPromise); + } else { + // Reuse the existing promise, use won't rethrow if the promise has settled. + use(statusPromiseRef); + } +} + +/** + * Suspends until the provider has finished reconciling. + * + * **DO NOT EXPORT PUBLICLY** + * @internal + * @param {Client} client the client to check for readiness + */ +export function suspendUntilReconciled(client: Client) { + use(isProviderReady(client)); +} + +async function isProviderReady(client: Client) { + const controller = new AbortController(); + try { + return await new Promise((resolve, reject) => { + client.addHandler(ProviderEvents.Ready, resolve, { signal: controller.signal }); + client.addHandler(ProviderEvents.Error, reject, { signal: controller.signal }); + }); + } finally { + controller.abort(); + } } diff --git a/packages/react/src/internal/use.ts b/packages/react/src/internal/use.ts new file mode 100644 index 000000000..186c832b9 --- /dev/null +++ b/packages/react/src/internal/use.ts @@ -0,0 +1,53 @@ +/// +// This function is adopted from https://github.com/vercel/swr +import React from 'react'; + +/** + * Extends a Promise-like value to include status tracking. + * The extra properties are used to manage the lifecycle of the Promise, indicating its current state. + * More information can be found in the React RFE for the use hook. + * @see https://github.com/reactjs/rfcs/pull/229 + */ +export type UsePromise = + Promise & { + status?: 'pending' | 'fulfilled' | 'rejected'; + value?: T; + reason?: unknown; + }; + +/** + * React.use is a React API that lets you read the value of a resource like a Promise or context. + * It was officially added in React 19, so needs to be polyfilled to support older React versions. + * @param {UsePromise} thenable A thenable object that represents a Promise-like value. + * @returns {unknown} The resolved value of the thenable or throws if it's still pending or rejected. + */ +export const use = + React.use || + // This extra generic is to avoid TypeScript mixing up the generic and JSX syntax + // and emitting an error. + // We assume that this is only for the `use(thenable)` case, not `use(context)`. + // https://github.com/facebook/react/blob/aed00dacfb79d17c53218404c52b1c7aa59c4a89/packages/react-server/src/ReactFizzThenable.js#L45 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ((thenable: UsePromise): T => { + switch (thenable.status) { + case 'pending': + throw thenable; + case 'fulfilled': + return thenable.value as T; + case 'rejected': + throw thenable.reason; + default: + thenable.status = 'pending'; + thenable.then( + (v) => { + thenable.status = 'fulfilled'; + thenable.value = v; + }, + (e) => { + thenable.status = 'rejected'; + thenable.reason = e; + }, + ); + throw thenable; + } + }); diff --git a/packages/react/src/provider/provider.tsx b/packages/react/src/provider/provider.tsx index 64da42fdb..35333db5f 100644 --- a/packages/react/src/provider/provider.tsx +++ b/packages/react/src/provider/provider.tsx @@ -31,7 +31,7 @@ type ProviderProps = { * @param {ProviderProps} properties props for the context provider * @returns {OpenFeatureProvider} context provider */ -export function OpenFeatureProvider({ client, domain, children, ...options }: ProviderProps) { +export function OpenFeatureProvider({ client, domain, children, ...options }: ProviderProps): JSX.Element { if (!client) { client = OpenFeature.getClient(domain); } diff --git a/packages/react/src/provider/use-open-feature-client-status.ts b/packages/react/src/provider/use-open-feature-client-status.ts index fac4a42b7..544cf5b54 100644 --- a/packages/react/src/provider/use-open-feature-client-status.ts +++ b/packages/react/src/provider/use-open-feature-client-status.ts @@ -10,22 +10,18 @@ import { ProviderEvents } from '@openfeature/web-sdk'; export function useOpenFeatureClientStatus(): ProviderStatus { const client = useOpenFeatureClient(); const [status, setStatus] = useState(client.providerStatus); + const controller = new AbortController(); useEffect(() => { const updateStatus = () => setStatus(client.providerStatus); - client.addHandler(ProviderEvents.ConfigurationChanged, updateStatus); - client.addHandler(ProviderEvents.ContextChanged, updateStatus); - client.addHandler(ProviderEvents.Error, updateStatus); - client.addHandler(ProviderEvents.Ready, updateStatus); - client.addHandler(ProviderEvents.Stale, updateStatus); - client.addHandler(ProviderEvents.Reconciling, updateStatus); + client.addHandler(ProviderEvents.ConfigurationChanged, updateStatus, { signal: controller.signal }); + client.addHandler(ProviderEvents.ContextChanged, updateStatus, { signal: controller.signal }); + client.addHandler(ProviderEvents.Error, updateStatus, { signal: controller.signal }); + client.addHandler(ProviderEvents.Ready, updateStatus, { signal: controller.signal }); + client.addHandler(ProviderEvents.Stale, updateStatus, { signal: controller.signal }); + client.addHandler(ProviderEvents.Reconciling, updateStatus, { signal: controller.signal }); return () => { - client.removeHandler(ProviderEvents.ConfigurationChanged, updateStatus); - client.removeHandler(ProviderEvents.ContextChanged, updateStatus); - client.removeHandler(ProviderEvents.Error, updateStatus); - client.removeHandler(ProviderEvents.Ready, updateStatus); - client.removeHandler(ProviderEvents.Stale, updateStatus); - client.removeHandler(ProviderEvents.Reconciling, updateStatus); + controller.abort(); }; }, [client]); diff --git a/packages/react/src/provider/use-open-feature-client.ts b/packages/react/src/provider/use-open-feature-client.ts index ecd776451..093fe1c5e 100644 --- a/packages/react/src/provider/use-open-feature-client.ts +++ b/packages/react/src/provider/use-open-feature-client.ts @@ -1,6 +1,7 @@ import React from 'react'; import { Context } from '../internal'; -import type { Client } from '@openfeature/web-sdk'; +import { type Client } from '@openfeature/web-sdk'; +import { MissingContextError } from '../internal/errors'; /** * Get the {@link Client} instance for this OpenFeatureProvider context. @@ -11,9 +12,7 @@ export function useOpenFeatureClient(): Client { const { client } = React.useContext(Context) || {}; if (!client) { - throw new Error( - 'No OpenFeature client available - components using OpenFeature must be wrapped with an . If you are seeing this in a test, see: https://openfeature.dev/docs/reference/technologies/client/web/react#testing', - ); + throw new MissingContextError('No OpenFeature client available'); } return client; diff --git a/packages/react/src/provider/use-open-feature-provider.ts b/packages/react/src/provider/use-open-feature-provider.ts new file mode 100644 index 000000000..f15d0321e --- /dev/null +++ b/packages/react/src/provider/use-open-feature-provider.ts @@ -0,0 +1,21 @@ +import React from 'react'; +import { Context } from '../internal'; +import { OpenFeature } from '@openfeature/web-sdk'; +import type { Provider } from '@openfeature/web-sdk'; +import { MissingContextError } from '../internal/errors'; + +/** + * Get the {@link Provider} bound to the domain specified in the OpenFeatureProvider context. + * Note that it isn't recommended to interact with the provider directly, but rather through + * an OpenFeature client. + * @returns {Provider} provider for this scope + */ +export function useOpenFeatureProvider(): Provider { + const openFeatureContext = React.useContext(Context); + + if (!openFeatureContext) { + throw new MissingContextError('No OpenFeature context available'); + } + + return OpenFeature.getProvider(openFeatureContext.domain); +} diff --git a/packages/react/src/provider/use-when-provider-ready.ts b/packages/react/src/provider/use-when-provider-ready.ts index 4cb5d0f0f..f66b2606c 100644 --- a/packages/react/src/provider/use-when-provider-ready.ts +++ b/packages/react/src/provider/use-when-provider-ready.ts @@ -2,7 +2,8 @@ import { ProviderStatus } from '@openfeature/web-sdk'; import { useOpenFeatureClient } from './use-open-feature-client'; import { useOpenFeatureClientStatus } from './use-open-feature-client-status'; import type { ReactFlagEvaluationOptions } from '../options'; -import { DEFAULT_OPTIONS, useProviderOptions, normalizeOptions, suspendUntilReady } from '../internal'; +import { DEFAULT_OPTIONS, useProviderOptions, normalizeOptions, suspendUntilInitialized } from '../internal'; +import { useOpenFeatureProvider } from './use-open-feature-provider'; type Options = Pick; @@ -14,14 +15,14 @@ type Options = Pick; * @returns {boolean} boolean indicating if provider is {@link ProviderStatus.READY}, useful if suspense is disabled and you want to handle loaders on your own */ export function useWhenProviderReady(options?: Options): boolean { - const client = useOpenFeatureClient(); - const status = useOpenFeatureClientStatus(); // highest priority > evaluation hook options > provider options > default options > lowest priority const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options) }; + const client = useOpenFeatureClient(); + const status = useOpenFeatureClientStatus(); + const provider = useOpenFeatureProvider(); - // suspense if (defaultedOptions.suspendUntilReady && status === ProviderStatus.NOT_READY) { - suspendUntilReady(client); + suspendUntilInitialized(provider, client); } return status === ProviderStatus.READY; diff --git a/packages/react/test/evaluation.spec.tsx b/packages/react/test/evaluation.spec.tsx index 5c9108c5a..5b08b30f5 100644 --- a/packages/react/test/evaluation.spec.tsx +++ b/packages/react/test/evaluation.spec.tsx @@ -6,12 +6,7 @@ import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/rea import { act, render, renderHook, screen, waitFor } from '@testing-library/react'; import * as React from 'react'; import { startTransition, useState } from 'react'; -import type { - EvaluationContext, - EvaluationDetails, - EventContext, - Hook -} from '../src/'; +import type { EvaluationContext, EvaluationDetails, EventContext, Hook } from '../src/'; import { ErrorCode, InMemoryProvider, @@ -27,15 +22,18 @@ import { useObjectFlagValue, useStringFlagDetails, useStringFlagValue, - useSuspenseFlag + useSuspenseFlag, } from '../src/'; -import { HookFlagQuery } from '../src/evaluation/hook-flag-query'; +import { HookFlagQuery } from '../src/internal/hook-flag-query'; import { TestingProvider } from './test.utils'; // custom provider to have better control over the emitted events class CustomEventInMemoryProvider extends InMemoryProvider { - - putConfigurationWithCustomEvent(flagConfiguration: FlagConfiguration, event: ProviderEmittableEvents, eventContext: EventContext) { + putConfigurationWithCustomEvent( + flagConfiguration: FlagConfiguration, + event: ProviderEmittableEvents, + eventContext: EventContext, + ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this['_flagConfiguration'] = { ...flagConfiguration }; // private access hack this.events.emit(event, eventContext); @@ -395,16 +393,19 @@ describe('evaluation', () => { expect(screen.queryByTestId('render-count')).toHaveTextContent('1'); await act(async () => { - await rerenderProvider.putConfigurationWithCustomEvent({ - ...FLAG_CONFIG, - [BOOL_FLAG_KEY]: { - ...FLAG_CONFIG[BOOL_FLAG_KEY], - // Change the default; this should be ignored and not cause a re-render because flagsChanged is empty - defaultVariant: 'off', + await rerenderProvider.putConfigurationWithCustomEvent( + { + ...FLAG_CONFIG, + [BOOL_FLAG_KEY]: { + ...FLAG_CONFIG[BOOL_FLAG_KEY], + // Change the default; this should be ignored and not cause a re-render because flagsChanged is empty + defaultVariant: 'off', + }, + // if the flagsChanged is empty, we know nothing has changed, so we don't bother diffing }, - // if the flagsChanged is empty, we know nothing has changed, so we don't bother diffing - }, ClientProviderEvents.ConfigurationChanged, { flagsChanged: [] }); - + ClientProviderEvents.ConfigurationChanged, + { flagsChanged: [] }, + ); }); expect(screen.queryByTestId('render-count')).toHaveTextContent('1'); @@ -420,16 +421,19 @@ describe('evaluation', () => { expect(screen.queryByTestId('render-count')).toHaveTextContent('1'); await act(async () => { - await rerenderProvider.putConfigurationWithCustomEvent({ - ...FLAG_CONFIG, - [BOOL_FLAG_KEY]: { - ...FLAG_CONFIG[BOOL_FLAG_KEY], - // Change the default variant to trigger a rerender since not only do we check flagsChanged, but we also diff the value - defaultVariant: 'off', + await rerenderProvider.putConfigurationWithCustomEvent( + { + ...FLAG_CONFIG, + [BOOL_FLAG_KEY]: { + ...FLAG_CONFIG[BOOL_FLAG_KEY], + // Change the default variant to trigger a rerender since not only do we check flagsChanged, but we also diff the value + defaultVariant: 'off', + }, + // if the flagsChanged is falsy, we don't know what flags changed - so we attempt to diff everything }, - // if the flagsChanged is falsy, we don't know what flags changed - so we attempt to diff everything - }, ClientProviderEvents.ConfigurationChanged, { flagsChanged: undefined }); - + ClientProviderEvents.ConfigurationChanged, + { flagsChanged: undefined }, + ); }); expect(screen.queryByTestId('render-count')).toHaveTextContent('2'); @@ -573,10 +577,41 @@ describe('evaluation', () => { }, }; + afterEach(() => { + OpenFeature.clearProviders(); + }); + const suspendingProvider = () => { return new TestingProvider(CONFIG, DELAY); // delay init by 100ms }; + describe('when using the noop provider', () => { + function TestComponent() { + const { value } = useSuspenseFlag(SUSPENSE_FLAG_KEY, DEFAULT); + return ( + <> +
{value}
+ + ); + } + it('should fallback to the default value on the next rerender', async () => { + render( + + {FALLBACK}}> + + + , + ); + // The loading indicator should be shown on the first render + expect(screen.queryByText(FALLBACK)).toBeInTheDocument(); + + // The default value should be shown on the next render + await waitFor(() => expect(screen.queryByText(DEFAULT)).toBeInTheDocument(), { + timeout: DELAY, + }); + }); + }); + describe('updateOnConfigurationChanged=true (default)', () => { function TestComponent() { const { value } = useFlag(SUSPENSE_FLAG_KEY, DEFAULT); diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index 1142bfecd..dc3ba8755 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.18.0](https://github.com/open-feature/js-sdk/compare/server-sdk-v1.17.1...server-sdk-v1.18.0) (2025-04-11) + + +### ✨ New Features + +* add a top-level method for accessing providers ([#1152](https://github.com/open-feature/js-sdk/issues/1152)) ([ae8fce8](https://github.com/open-feature/js-sdk/commit/ae8fce87530005ed20f7e68dc696ce67053fca31)) +* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038)) + ## [1.17.1](https://github.com/open-feature/js-sdk/compare/server-sdk-v1.17.0...server-sdk-v1.17.1) (2025-02-07) diff --git a/packages/server/README.md b/packages/server/README.md index 9069395c7..748e2de9c 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -16,8 +16,8 @@ Specification - - Release + + Release
diff --git a/packages/server/package.json b/packages/server/package.json index 0655c7667..8e039cfdc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/server-sdk", - "version": "1.17.1", + "version": "1.18.0", "description": "OpenFeature SDK for JavaScript", "main": "./dist/cjs/index.js", "files": [ diff --git a/packages/server/src/client/internal/open-feature-client.ts b/packages/server/src/client/internal/open-feature-client.ts index 08a577845..4f440e940 100644 --- a/packages/server/src/client/internal/open-feature-client.ts +++ b/packages/server/src/client/internal/open-feature-client.ts @@ -12,6 +12,7 @@ import type { OpenFeatureError, FlagMetadata, ResolutionDetails, + EventOptions, } from '@openfeature/core'; import { ErrorCode, @@ -79,7 +80,7 @@ export class OpenFeatureClient implements Client { return this.providerStatusAccessor(); } - addHandler(eventType: ProviderEvents, handler: EventHandler): void { + addHandler(eventType: ProviderEvents, handler: EventHandler, options?: EventOptions): void { this.emitterAccessor().addHandler(eventType, handler); const shouldRunNow = statusMatchesEvent(eventType, this._providerStatus); @@ -95,6 +96,12 @@ export class OpenFeatureClient implements Client { this._logger?.error('Error running event handler:', err); } } + + if (options?.signal && typeof options.signal.addEventListener === 'function') { + options.signal.addEventListener('abort', () => { + this.removeHandler(eventType, handler); + }); + } } removeHandler(eventType: ProviderEvents, handler: EventHandler) { diff --git a/packages/server/src/open-feature.ts b/packages/server/src/open-feature.ts index ae4b439f0..3e818af05 100644 --- a/packages/server/src/open-feature.ts +++ b/packages/server/src/open-feature.ts @@ -138,6 +138,27 @@ export class OpenFeatureAPI return this; } + /** + * Get the default provider. + * + * Note that it isn't recommended to interact with the provider directly, but rather through + * an OpenFeature client. + * @returns {Provider} Default Provider + */ + getProvider(): Provider; + /** + * Get the provider bound to the specified domain. + * + * Note that it isn't recommended to interact with the provider directly, but rather through + * an OpenFeature client. + * @param {string} domain An identifier which logically binds clients with providers + * @returns {Provider} Domain-scoped provider + */ + getProvider(domain?: string): Provider; + getProvider(domain?: string): Provider { + return this.getProviderForClient(domain); + } + setContext(context: EvaluationContext): this { this._context = context; return this; diff --git a/packages/server/test/events.spec.ts b/packages/server/test/events.spec.ts index 07b3babe3..010a0f33d 100644 --- a/packages/server/test/events.spec.ts +++ b/packages/server/test/events.spec.ts @@ -449,7 +449,21 @@ describe('Events', () => { expect(OpenFeature.getHandlers(eventType)).toHaveLength(0); }); - it('The API provides a function allowing the removal of event handlers', () => { + it('The event handler can be removed using an abort signal', () => { + const abortController = new AbortController(); + const handler1 = jest.fn(); + const handler2 = jest.fn(); + const eventType = ProviderEvents.Stale; + + OpenFeature.addHandler(eventType, handler1, { signal: abortController.signal }); + OpenFeature.addHandler(eventType, handler2); + expect(OpenFeature.getHandlers(eventType)).toHaveLength(2); + + abortController.abort(); + expect(OpenFeature.getHandlers(eventType)).toHaveLength(1); + }); + + it('The API provides a function allowing the removal of event handlers from client', () => { const client = OpenFeature.getClient(domain); const handler = jest.fn(); const eventType = ProviderEvents.Stale; @@ -459,6 +473,21 @@ describe('Events', () => { client.removeHandler(eventType, handler); expect(client.getHandlers(eventType)).toHaveLength(0); }); + + it('The event handler on the client can be removed using an abort signal', () => { + const abortController = new AbortController(); + const client = OpenFeature.getClient(domain); + const handler1 = jest.fn(); + const handler2 = jest.fn(); + const eventType = ProviderEvents.Stale; + + client.addHandler(eventType, handler1, { signal: abortController.signal }); + client.addHandler(eventType, handler2); + expect(client.getHandlers(eventType)).toHaveLength(2); + + abortController.abort(); + expect(client.getHandlers(eventType)).toHaveLength(1); + }); }); describe('Requirement 5.3.1', () => { diff --git a/packages/server/test/open-feature.spec.ts b/packages/server/test/open-feature.spec.ts index fb83f0c91..fe41f8354 100644 --- a/packages/server/test/open-feature.spec.ts +++ b/packages/server/test/open-feature.spec.ts @@ -74,8 +74,8 @@ describe('OpenFeature', () => { it('should set the default provider if no domain is provided', () => { const provider = mockProvider(); OpenFeature.setProvider(provider); - const client = OpenFeature.getClient(); - expect(client.metadata.providerMetadata.name).toEqual(provider.metadata.name); + const registeredProvider = OpenFeature.getProvider(); + expect(registeredProvider).toEqual(provider); }); it('should not change providers associated with a domain when setting a new default provider', () => { @@ -85,11 +85,11 @@ describe('OpenFeature', () => { OpenFeature.setProvider(provider); OpenFeature.setProvider(domain, fakeProvider); - const defaultClient = OpenFeature.getClient(); - const domainSpecificClient = OpenFeature.getClient(domain); + const defaultProvider = OpenFeature.getProvider(); + const domainSpecificProvider = OpenFeature.getProvider(domain); - expect(defaultClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); - expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(defaultProvider).toEqual(provider); + expect(domainSpecificProvider).toEqual(fakeProvider); }); it('should bind a new provider to existing clients in a matching domain', () => { diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md index 5ed5c2a66..1c5f9f6bb 100644 --- a/packages/shared/CHANGELOG.md +++ b/packages/shared/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [1.8.0](https://github.com/open-feature/js-sdk/compare/core-v1.7.2...core-v1.8.0) (2025-04-10) + + +### ✨ New Features + +* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038)) + +## [1.7.2](https://github.com/open-feature/js-sdk/compare/core-v1.7.1...core-v1.7.2) (2025-02-18) + + +### 🐛 Bug Fixes + +* rename evaluation event property from data to body ([4c2b01e](https://github.com/open-feature/js-sdk/commit/4c2b01e36773091038d758ac10bba06056ff4c45)) + +## [1.7.1](https://github.com/open-feature/js-sdk/compare/core-v1.7.0...core-v1.7.1) (2025-02-13) + + +### 🐛 Bug Fixes + +* export missing telemetry functionality ([#1148](https://github.com/open-feature/js-sdk/issues/1148)) ([dcbc300](https://github.com/open-feature/js-sdk/commit/dcbc30090e7611c60e06d05826f6471f0c8c4009)) + ## [1.7.0](https://github.com/open-feature/js-sdk/compare/core-v1.6.0...core-v1.7.0) (2025-02-07) diff --git a/packages/shared/package.json b/packages/shared/package.json index dd812242c..43416ee6c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/core", - "version": "1.7.0", + "version": "1.8.0", "description": "Shared OpenFeature JS components (server and web)", "main": "./dist/cjs/index.js", "files": [ diff --git a/packages/shared/src/events/eventing.ts b/packages/shared/src/events/eventing.ts index 976e12cf4..c3ae8b9db 100644 --- a/packages/shared/src/events/eventing.ts +++ b/packages/shared/src/events/eventing.ts @@ -66,6 +66,9 @@ export type EventDetails< export type EventHandler< T extends ServerProviderEvents | ClientProviderEvents = ServerProviderEvents | ClientProviderEvents, > = (eventDetails?: EventDetails) => Promise | unknown; +export type EventOptions = { + signal?: AbortSignal; +}; export interface Eventing { /** @@ -73,6 +76,7 @@ export interface Eventing * The handlers are called in the order they have been added. * @param eventType The provider event type to listen to * @param {EventHandler} handler The handler to run on occurrence of the event type + * @param {EventOptions} options Optional options such as signal for aborting */ addHandler( eventType: T extends ClientProviderEvents @@ -83,14 +87,17 @@ export interface Eventing ? ClientProviderEvents.ConfigurationChanged : ServerProviderEvents.ConfigurationChanged >, + options?: EventOptions, ): void; addHandler( eventType: T extends ClientProviderEvents ? ClientNotChangeEvents : ServerNotChangeEvents, handler: EventHandler, + options?: EventOptions, ): void; addHandler( eventType: T extends ClientProviderEvents ? ClientProviderEvents : ServerProviderEvents, handler: EventHandler, + options?: EventOptions, ): void; /** diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 42c1504a0..c708fb9af 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -4,6 +4,7 @@ export * from './client'; export * from './errors'; export * from './events'; export * from './logger'; +export * from './telemetry'; export * from './provider'; export * from './evaluation'; export * from './type-guards'; diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts index 58e1e6a14..fc82bc908 100644 --- a/packages/shared/src/open-feature.ts +++ b/packages/shared/src/open-feature.ts @@ -7,14 +7,13 @@ import type { EventDetails, EventHandler, Eventing, - GenericEventEmitter} from './events'; -import { - AllProviderEvents, - statusMatchesEvent, + EventOptions, + GenericEventEmitter, } from './events'; +import { AllProviderEvents, statusMatchesEvent } from './events'; import { isDefined } from './filter'; import type { BaseHook, EvaluationLifeCycle } from './hooks'; -import type { Logger, ManageLogger} from './logger'; +import type { Logger, ManageLogger } from './logger'; import { DefaultLogger, SafeLogger } from './logger'; import type { ClientProviderStatus, CommonProvider, ProviderMetadata, ServerProviderStatus } from './provider'; import { objectOrUndefined, stringOrUndefined } from './type-guards'; @@ -154,8 +153,9 @@ export abstract class OpenFeatureCommonAPI< * API (global) events run for all providers. * @param {AnyProviderEvent} eventType The provider event type to listen to * @param {EventHandler} handler The handler to run on occurrence of the event type + * @param {EventOptions} options Optional options such as signal for aborting */ - addHandler(eventType: T, handler: EventHandler): void { + addHandler(eventType: T, handler: EventHandler, options?: EventOptions): void { [...new Map([[undefined, this._defaultProvider]]), ...this._domainScopedProviders].forEach((keyProviderTuple) => { const domain = keyProviderTuple[0]; const provider = keyProviderTuple[1].provider; @@ -173,6 +173,11 @@ export abstract class OpenFeatureCommonAPI< }); this._apiEmitter.addHandler(eventType, handler); + if (options?.signal && typeof options.signal.addEventListener === 'function') { + options.signal.addEventListener('abort', () => { + this.removeHandler(eventType, handler); + }); + } } /** @@ -248,7 +253,7 @@ export abstract class OpenFeatureCommonAPI< // initialize the provider if it implements "initialize" and it's not already registered if (typeof provider.initialize === 'function' && !this.allProviders.includes(provider)) { initializationPromise = provider - .initialize?.(domain ? this._domainScopedContext.get(domain) ?? this._context : this._context) + .initialize?.(domain ? (this._domainScopedContext.get(domain) ?? this._context) : this._context) ?.then(() => { wrappedProvider.status = this._statusEnumType.READY; // fetch the most recent event emitters, some may have been added during init diff --git a/packages/shared/src/telemetry/evaluation-event.ts b/packages/shared/src/telemetry/evaluation-event.ts index 91bfc2d1e..3ee1630d7 100644 --- a/packages/shared/src/telemetry/evaluation-event.ts +++ b/packages/shared/src/telemetry/evaluation-event.ts @@ -8,7 +8,7 @@ import { TelemetryFlagMetadata } from './flag-metadata'; type EvaluationEvent = { name: string; attributes: Record; - data: Record; + body: Record; }; const FLAG_EVALUATION_EVENT_NAME = 'feature_flag.evaluation'; @@ -28,12 +28,12 @@ export function createEvaluationEvent( [TelemetryAttribute.PROVIDER]: hookContext.providerMetadata.name, [TelemetryAttribute.REASON]: (evaluationDetails.reason ?? StandardResolutionReasons.UNKNOWN).toLowerCase(), }; - const data: EvaluationEvent['data'] = {}; + const body: EvaluationEvent['body'] = {}; if (evaluationDetails.variant) { attributes[TelemetryAttribute.VARIANT] = evaluationDetails.variant; } else { - data[TelemetryEvaluationData.VALUE] = evaluationDetails.value; + body[TelemetryEvaluationData.VALUE] = evaluationDetails.value; } const contextId = @@ -62,6 +62,6 @@ export function createEvaluationEvent( return { name: FLAG_EVALUATION_EVENT_NAME, attributes, - data, + body, }; } diff --git a/packages/shared/test/events.spec.ts b/packages/shared/test/events.spec.ts index f490892a9..a5597329f 100644 --- a/packages/shared/test/events.spec.ts +++ b/packages/shared/test/events.spec.ts @@ -14,17 +14,23 @@ class TestEventEmitter extends GenericEventEmitter { } } -// a little function to make sure we're at least waiting for the event loop +// a little function to make sure we're at least waiting for the event loop // to clear before we start making assertions const wait = (millis = 0) => { - return new Promise(resolve => {setTimeout(resolve, millis);}); + return new Promise((resolve) => { + setTimeout(resolve, millis); + }); }; describe('GenericEventEmitter', () => { + const emitter = new TestEventEmitter(); + + afterEach(() => { + emitter.removeAllHandlers(); + }); + describe('addHandler should', function () { it('attach handler for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); emitter.emit(AllProviderEvents.Ready); @@ -35,8 +41,6 @@ describe('GenericEventEmitter', () => { }); it('attach several handlers for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); const handler2 = jest.fn(); const handler3 = jest.fn(); @@ -64,7 +68,6 @@ describe('GenericEventEmitter', () => { debug: () => done(), }; - const emitter = new TestEventEmitter(); emitter.setLogger(logger); emitter.addHandler(AllProviderEvents.Ready, async () => { @@ -74,8 +77,6 @@ describe('GenericEventEmitter', () => { }); it('trigger handler for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); emitter.emit(AllProviderEvents.Ready); @@ -87,7 +88,6 @@ describe('GenericEventEmitter', () => { it('trigger handler for event type with event data', async function () { const event: ReadyEvent = { message: 'message' }; - const emitter = new TestEventEmitter(); const handler1 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); @@ -99,8 +99,6 @@ describe('GenericEventEmitter', () => { }); it('trigger several handlers for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); const handler2 = jest.fn(); const handler3 = jest.fn(); @@ -121,8 +119,6 @@ describe('GenericEventEmitter', () => { describe('removeHandler should', () => { it('remove single handler', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); @@ -138,8 +134,6 @@ describe('GenericEventEmitter', () => { describe('removeAllHandlers should', () => { it('remove all handlers for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); const handler2 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); @@ -156,8 +150,6 @@ describe('GenericEventEmitter', () => { }); it('remove same handler when assigned to multiple events', async function () { - const emitter = new TestEventEmitter(); - const handler = jest.fn(); emitter.addHandler(AllProviderEvents.Stale, handler); emitter.addHandler(AllProviderEvents.ContextChanged, handler); @@ -174,8 +166,6 @@ describe('GenericEventEmitter', () => { }); it('allow addition/removal of duplicate handlers', async function () { - const emitter = new TestEventEmitter(); - const handler = jest.fn(); emitter.addHandler(AllProviderEvents.Stale, handler); emitter.addHandler(AllProviderEvents.Stale, handler); @@ -191,8 +181,6 @@ describe('GenericEventEmitter', () => { }); it('allow duplicate event handlers and call them', async function () { - const emitter = new TestEventEmitter(); - const handler = jest.fn(); emitter.addHandler(AllProviderEvents.Stale, handler); emitter.addHandler(AllProviderEvents.Stale, handler); @@ -205,8 +193,6 @@ describe('GenericEventEmitter', () => { }); it('remove all handlers only for event type', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); const handler2 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); @@ -223,8 +209,6 @@ describe('GenericEventEmitter', () => { }); it('remove all handlers if no event type is given', async function () { - const emitter = new TestEventEmitter(); - const handler1 = jest.fn(); const handler2 = jest.fn(); emitter.addHandler(AllProviderEvents.Ready, handler1); diff --git a/packages/shared/test/telemetry.spec.ts b/packages/shared/test/telemetry.spec.ts index c105d29e7..62d9871cf 100644 --- a/packages/shared/test/telemetry.spec.ts +++ b/packages/shared/test/telemetry.spec.ts @@ -44,7 +44,7 @@ describe('evaluationEvent', () => { [TelemetryAttribute.REASON]: StandardResolutionReasons.STATIC.toLowerCase(), [TelemetryAttribute.CONTEXT_ID]: 'test-target', }); - expect(result.data).toEqual({ + expect(result.body).toEqual({ [TelemetryEvaluationData.VALUE]: true, }); }); diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index ba426c32f..760896a54 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog +## [1.5.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.4.1...web-sdk-v1.5.0) (2025-04-11) + + +### ✨ New Features + +* add a top-level method for accessing providers ([#1152](https://github.com/open-feature/js-sdk/issues/1152)) ([ae8fce8](https://github.com/open-feature/js-sdk/commit/ae8fce87530005ed20f7e68dc696ce67053fca31)) +* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038)) + + +### 🐛 Bug Fixes + +* Typo in name of the function ([2c5b37c](https://github.com/open-feature/js-sdk/commit/2c5b37c79d72d60864c27b9e67d96e99ef4ae241)) + ## [1.4.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.4.0...web-sdk-v1.4.1) (2025-02-07) diff --git a/packages/web/README.md b/packages/web/README.md index e84d9446b..08c022fd7 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -16,8 +16,8 @@ Specification - - Release + + Release
@@ -172,7 +172,7 @@ await OpenFeature.setContext({ targetingKey: localStorage.getItem("targetingKey" ``` Context is global and setting it is `async`. -Providers may implement an `onContextChanged` method that receives the old and newer contexts. +Providers may implement an `onContextChange` method that receives the old and newer contexts. Given a context change, providers can use this method internally to detect if the flag values cached on the client are still valid. If needed, a request will be made to the provider with the new context in order to get the correct flag values. diff --git a/packages/web/package.json b/packages/web/package.json index 39067d78a..2e071ef2b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@openfeature/web-sdk", - "version": "1.4.1", + "version": "1.5.0", "description": "OpenFeature SDK for Web", "main": "./dist/cjs/index.js", "files": [ @@ -46,9 +46,9 @@ }, "homepage": "https://github.com/open-feature/js-sdk#readme", "peerDependencies": { - "@openfeature/core": "^1.7.0" + "@openfeature/core": "^1.8.0" }, "devDependencies": { - "@openfeature/core": "^1.7.0" + "@openfeature/core": "^1.8.0" } } diff --git a/packages/web/src/client/internal/open-feature-client.ts b/packages/web/src/client/internal/open-feature-client.ts index 0e0379f8e..7eed9a9a6 100644 --- a/packages/web/src/client/internal/open-feature-client.ts +++ b/packages/web/src/client/internal/open-feature-client.ts @@ -12,6 +12,7 @@ import type { OpenFeatureError, FlagMetadata, ResolutionDetails, + EventOptions, } from '@openfeature/core'; import { ErrorCode, @@ -74,7 +75,7 @@ export class OpenFeatureClient implements Client { return this.providerStatusAccessor(); } - addHandler(eventType: ProviderEvents, handler: EventHandler): void { + addHandler(eventType: ProviderEvents, handler: EventHandler, options: EventOptions): void { this.emitterAccessor().addHandler(eventType, handler); const shouldRunNow = statusMatchesEvent(eventType, this.providerStatus); @@ -90,6 +91,12 @@ export class OpenFeatureClient implements Client { this._logger?.error('Error running event handler:', err); } } + + if (options?.signal && typeof options.signal.addEventListener === 'function') { + options.signal.addEventListener('abort', () => { + this.removeHandler(eventType, handler); + }); + } } removeHandler(notificationType: ProviderEvents, handler: EventHandler): void { diff --git a/packages/web/src/open-feature.ts b/packages/web/src/open-feature.ts index 9c35a31a4..eb32877db 100644 --- a/packages/web/src/open-feature.ts +++ b/packages/web/src/open-feature.ts @@ -205,6 +205,27 @@ export class OpenFeatureAPI return this; } + /** + * Get the default provider. + * + * Note that it isn't recommended to interact with the provider directly, but rather through + * an OpenFeature client. + * @returns {Provider} Default Provider + */ + getProvider(): Provider; + /** + * Get the provider bound to the specified domain. + * + * Note that it isn't recommended to interact with the provider directly, but rather through + * an OpenFeature client. + * @param {string} domain An identifier which logically binds clients with providers + * @returns {Provider} Domain-scoped provider + */ + getProvider(domain?: string): Provider; + getProvider(domain?: string): Provider { + return this.getProviderForClient(domain); + } + /** * Sets the evaluation context globally. * This will be used by all providers that have not bound to a domain. @@ -325,9 +346,9 @@ export class OpenFeatureAPI } /** - * A factory function for creating new named OpenFeature clients. Clients can contain - * their own state (e.g. logger, hook, context). Multiple clients can be used - * to segment feature flag configuration. + * A factory function for creating new domain-scoped OpenFeature clients. Clients + * can contain their own state (e.g. logger, hook, context). Multiple domains + * can be used to segment feature flag configuration. * * If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used. * Otherwise, the default provider is used until a provider is assigned to that name. diff --git a/packages/web/test/events.spec.ts b/packages/web/test/events.spec.ts index ea020b438..208b027fc 100644 --- a/packages/web/test/events.spec.ts +++ b/packages/web/test/events.spec.ts @@ -476,7 +476,21 @@ describe('Events', () => { expect(OpenFeature.getHandlers(eventType)).toHaveLength(0); }); - it('The API provides a function allowing the removal of event handlers', () => { + it('The event handler can be removed using an abort signal', () => { + const abortController = new AbortController(); + const handler1 = jest.fn(); + const handler2 = jest.fn(); + const eventType = ProviderEvents.Stale; + + OpenFeature.addHandler(eventType, handler1, { signal: abortController.signal }); + OpenFeature.addHandler(eventType, handler2); + expect(OpenFeature.getHandlers(eventType)).toHaveLength(2); + + abortController.abort(); + expect(OpenFeature.getHandlers(eventType)).toHaveLength(1); + }); + + it('The API provides a function allowing the removal of event handlers from client', () => { const client = OpenFeature.getClient(domain); const handler = jest.fn(); const eventType = ProviderEvents.Stale; @@ -486,6 +500,21 @@ describe('Events', () => { client.removeHandler(eventType, handler); expect(client.getHandlers(eventType)).toHaveLength(0); }); + + it('The event handler on the client can be removed using an abort signal', () => { + const abortController = new AbortController(); + const client = OpenFeature.getClient(domain); + const handler1 = jest.fn(); + const handler2 = jest.fn(); + const eventType = ProviderEvents.Stale; + + client.addHandler(eventType, handler1, { signal: abortController.signal }); + client.addHandler(eventType, handler2); + expect(client.getHandlers(eventType)).toHaveLength(2); + + abortController.abort(); + expect(client.getHandlers(eventType)).toHaveLength(1); + }); }); describe('Requirement 5.3.1', () => { diff --git a/packages/web/test/open-feature.spec.ts b/packages/web/test/open-feature.spec.ts index 2e2f32b69..bf0589ca7 100644 --- a/packages/web/test/open-feature.spec.ts +++ b/packages/web/test/open-feature.spec.ts @@ -75,8 +75,8 @@ describe('OpenFeature', () => { it('should set the default provider if no domain is provided', () => { const provider = mockProvider(); OpenFeature.setProvider(provider); - const client = OpenFeature.getClient(); - expect(client.metadata.providerMetadata.name).toEqual(provider.metadata.name); + const registeredProvider = OpenFeature.getProvider(); + expect(registeredProvider).toEqual(provider); }); it('should not change providers associated with a domain when setting a new default provider', () => { @@ -86,11 +86,11 @@ describe('OpenFeature', () => { OpenFeature.setProvider(provider); OpenFeature.setProvider(domain, fakeProvider); - const defaultClient = OpenFeature.getClient(); - const domainSpecificClient = OpenFeature.getClient(domain); + const defaultProvider = OpenFeature.getProvider(); + const domainSpecificProvider = OpenFeature.getProvider(domain); - expect(defaultClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); - expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(defaultProvider).toEqual(provider); + expect(domainSpecificProvider).toEqual(fakeProvider); }); it('should bind a new provider to existing clients in a matching domain', () => { diff --git a/release-please-config.json b/release-please-config.json index 6d4574698..cbea531c5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -20,6 +20,7 @@ "versioning": "default" }, "packages/react": { + "release-as": "1.0.0", "release-type": "node", "prerelease": false, "bump-minor-pre-major": true, @@ -29,7 +30,7 @@ }, "packages/angular/projects/angular-sdk": { "release-type": "node", - "prerelease": true, + "prerelease": false, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "extra-files": ["README.md"],