Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"libs/providers/unleash-web": "0.1.1",
"libs/providers/growthbook": "0.1.2",
"libs/providers/aws-ssm": "0.1.3",
"libs/providers/flagsmith": "0.1.2"
"libs/providers/flagsmith": "0.1.2",
"libs/hooks/debounce": "0.1.0"
}
30 changes: 30 additions & 0 deletions libs/hooks/debounce/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"extends": "../../../.eslintrc.json",
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
}
]
}
}
]
}
49 changes: 49 additions & 0 deletions libs/hooks/debounce/README.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to target the web only, please make that very clear at the top of the file.

We could consider making this work on the server and client, like @lukas-reining did for the telemetry hooks. Since this hook is so flexible, I could see some use cases on the server, specifically when running it with a logging hook.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I see no reason why this can't be for both, I can make that change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!
We need to cache the return value of the before hook too then. But if we cache all stages together as proposed for the other issue it does not matter anymore :)

Copy link
Member Author

@toddbaert toddbaert Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is actually possible.

The problem is that server hooks are possibly async, so to cache the return value of the before hook (and also to generally handle async errors) the inner hook as to be awaited (forcing the debounce hook to be async) - but the web doesn't support async hooks. It's basically the same reason we don't have a shared hook implementation in the web/server SDKs (only a BaseHook type) - the async support of server is incompatible with the web hook. This isn't a problem for the OTel hooks because they are fire-and-forget no nothing is actually awaited.

I COULD have a shared base version of this package and server/web version that shared most code, but like I said... I think the same thing preventing us from having a single Hook in both SDKs prevents us from creating a single version of this hook for both SDKs.

Open to suggestions here, or to be told I'm wrong.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Debounce Hook

This is a utility "meta" hook, which can be used to effectively debounce or rate limit other hooks based on various parameters.
This can be especially useful for certain UI frameworks and SDKs that frequently re-render and re-evaluate flags (React, Angular, etc).

## Installation

```
$ npm install @openfeature/debounce-hook
```

### Peer dependencies

Confirm that the following peer dependencies are installed:

```
$ npm install @openfeature/web-sdk
```

NOTE: if you're using the React or Angular OpenFeature SDKs, you don't need to directly install the web SDK.

## Usage

The hook maintains a simple expiring cache with a fixed max size and keeps a record of recent evaluations based on a user-defined key-generation function (keySupplier).
Simply wrap your hook with the debounce hook by passing it a constructor arg, and then configure the remaining options.
In the example below, we wrap a logging hook. This debounces all its stages, so it only logs a maximum of once a minute for each flag key, no matter how many times that flag is evaluated.

```ts
const debounceHook = new DebounceHook<string>(loggingHook, {
debounceTime: 60_000, // how long to wait before the hook can fire again
maxCacheItems: 100, // max amount of items to keep in the cache; if exceeded, the oldest item is dropped
});

// add the hook globally
OpenFeature.addHooks(debounceHook);

// or at a specific client
client.addHooks(debounceHook);
```

## Development

### Building

Run `nx package hooks-debounce` to build the library.

### Running unit tests

Run `nx test hooks-debounce` to execute the unit tests via [Jest](https://jestjs.io).
3 changes: 3 additions & 0 deletions libs/hooks/debounce/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["minify", { "builtIns": false }]]
}
9 changes: 9 additions & 0 deletions libs/hooks/debounce/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
displayName: 'debounce',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../coverage/hooks',
};
17 changes: 17 additions & 0 deletions libs/hooks/debounce/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@openfeature/debounce-hook",
"version": "0.0.1",
"dependencies": {
"tslib": "^2.3.0"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"scripts": {
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
"current-version": "echo $npm_package_version"
},
"license": "Apache-2.0",
"peerDependencies": {
"@openfeature/web-sdk": "^1.6.0"
}
}
77 changes: 77 additions & 0 deletions libs/hooks/debounce/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"name": "debounce",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "hooks/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": [],
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "{projectRoot}/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/hooks/debounce/package.json",
"outputPath": "dist/libs/hooks/debounce",
"entryFile": "libs/hooks/debounce/src/index.ts",
"tsConfig": "libs/hooks/debounce/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "debounce",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/hooks/debounce",
"output": "./"
}
]
}
},
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/hooks/debounce"
},
"dependsOn": [
{
"projects": "self",
"target": "package"
}
]
}
}
}
1 change: 1 addition & 0 deletions libs/hooks/debounce/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/debounce-hook';
Loading