Skip to content

Commit e1e77eb

Browse files
authored
feat(node-sdk): Cloudflare worker support (#436)
This PR adds Cloudflare Worker support to the Node SDK by introducing an edge-optimized cache strategy, updating the client to select between cache strategies, and providing example configurations and documentation. - Introduce inRequestCache and rename the existing cache to periodicallyUpdatingCache - Add EdgeClient class with "in-request" cache strategy and update BucketClient to choose based on cacheStrategy - Supply Cloudflare Worker examples, configuration files, and update documentation
1 parent affbb5f commit e1e77eb

37 files changed

Lines changed: 13071 additions & 139 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Browser SDK for use in non-React web applications
2323
## Node.js SDK
2424

2525
Node.js SDK for use on the server side.
26+
Use this for Cloudflare Workers as well.
2627

2728
[Read the docs](packages/node-sdk/README.md)
2829

packages/node-sdk/README.md

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Bucket supports feature toggling, tracking feature usage, collecting feedback on
66

77
## Installation
88

9-
Install using your favourite package manager:
9+
Install using your favorite package manager:
1010

1111
{% tabs %}
1212
{% tab title="npm" %}
@@ -207,6 +207,45 @@ const featureDefs = await client.getFeatureDefinitions();
207207
// }]
208208
```
209209

210+
## Edge-runtimes like Cloudflare Workers
211+
212+
To use the Bucket NodeSDK with Cloudflare workers, set the `node_compat` flag [in your wrangler file](https://developers.cloudflare.com/workers/runtime-apis/nodejs/#get-started).
213+
214+
Instead of using `BucketClient`, use `EdgeClient` and make sure you call `ctx.waitUntil(bucket.flush());` before returning from your worker function.
215+
216+
```typescript
217+
import { EdgeClient } from "@bucketco/node-sdk";
218+
219+
// set the BUCKET_SECRET_KEY environment variable or pass the secret key in the constructor
220+
const bucket = new EdgeClient();
221+
222+
export default {
223+
async fetch(request, _env, ctx): Promise<Response> {
224+
// initialize the client and wait for it to complete
225+
// if the client was initialized on a previous invocation, this is a no-op.
226+
await bucket.initialize();
227+
const features = bucket.getFeatures({
228+
user: { id: "userId" },
229+
company: { id: "companyId" },
230+
});
231+
232+
// ensure all events are flushed and any requests to refresh the feature cache
233+
// have completed after the response is sent
234+
ctx.waitUntil(bucket.flush());
235+
236+
return new Response(
237+
`Features for user ${userId} and company ${companyId}: ${JSON.stringify(features, null, 2)}`,
238+
);
239+
},
240+
};
241+
```
242+
243+
See [examples/cloudflare-worker](examples/cloudflare-worker/src/index.ts) for a deployable example.
244+
245+
Bucket maintains a cached set of feature definitions in the memory of your worker which it uses to decide which features to turn on for which users/companies.
246+
247+
The SDK caches feature definitions in memory for fast performance. The first request to a new worker instance fetches definitions from Bucket's servers, while subsequent requests use the cache. When the cache expires, it's updated in the background. `ctx.waitUntil(bucket.flush())` ensures completion of the background work, so response times are not affected. This background work may increase wall-clock time for your worker, but it will not measurably increase billable CPU time on platforms like Cloudflare.
248+
210249
## Error Handling
211250

212251
The SDK is designed to fail gracefully and never throw exceptions to the caller. Instead, it logs errors and provides
@@ -307,14 +346,14 @@ a configuration file on disk or by passing options to the `BucketClient`
307346
constructor. By default, the SDK searches for `bucketConfig.json` in the
308347
current working directory.
309348

310-
| Option | Type | Description | Env Var |
311-
| ------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
312-
| `secretKey` | string | The secret key used for authentication with Bucket's servers. | BUCKET_SECRET_KEY |
313-
| `logLevel` | string | The log level for the SDK (e.g., `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`). Default: `INFO` | BUCKET_LOG_LEVEL |
314-
| `offline` | boolean | Operate in offline mode. Default: `false`, except in tests it will default to `true` based off of the `TEST` env. var. | BUCKET_OFFLINE |
315-
| `apiBaseUrl` | string | The base API URL for the Bucket servers. | BUCKET_API_BASE_URL |
316-
| `featureOverrides` | Record<string, boolean> | An object specifying feature overrides for testing or local development. See [example/app.test.ts](https://github.com/bucketco/bucket-javascript-sdk/tree/main/packages/browser-sdk/example/app.test.ts) for how to use `featureOverrides` in tests. | BUCKET_FEATURES_ENABLED, BUCKET_FEATURES_DISABLED |
317-
| `configFile` | string | Load this config file from disk. Default: `bucketConfig.json` | BUCKET_CONFIG_FILE |
349+
| Option | Type | Description | Env Var |
350+
| ------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
351+
| `secretKey` | string | The secret key used for authentication with Bucket's servers. | BUCKET_SECRET_KEY |
352+
| `logLevel` | string | The log level for the SDK (e.g., `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`). Default: `INFO` | BUCKET_LOG_LEVEL |
353+
| `offline` | boolean | Operate in offline mode. Default: `false`, except in tests it will default to `true` based off of the `TEST` env. var. | BUCKET_OFFLINE |
354+
| `apiBaseUrl` | string | The base API URL for the Bucket servers. | BUCKET_API_BASE_URL |
355+
| `featureOverrides` | Record<string, boolean> | An object specifying feature overrides for testing or local development. See [examples/express/app.test.ts](https://github.com/bucketco/bucket-javascript-sdk/tree/main/packages/node-sdk/examples/express/app.test.ts) for how to use `featureOverrides` in tests. | BUCKET_FEATURES_ENABLED, BUCKET_FEATURES_DISABLED |
356+
| `configFile` | string | Load this config file from disk. Default: `bucketConfig.json` | BUCKET_CONFIG_FILE |
318357

319358
> [!NOTE] > `BUCKET_FEATURES_ENABLED` and `BUCKET_FEATURES_DISABLED` are comma separated lists of features which will be enabled or disabled respectively.
320359
@@ -616,7 +655,7 @@ app.get("/todos", async (_req, res) => {
616655
}
617656
```
618657
619-
See [example/app.ts](https://github.com/bucketco/bucket-javascript-sdk/tree/main/packages/node-sdk/example/app.ts) for a full example.
658+
See [examples/express/app.ts](https://github.com/bucketco/bucket-javascript-sdk/tree/main/packages/node-sdk/example/express/app.ts) for a full example.
620659
621660
## Remote flag evaluation with stored context
622661
@@ -687,11 +726,8 @@ these functions.
687726
688727
## Flushing
689728
690-
It is highly recommended that users of this SDK manually call `flush()`
691-
method on process shutdown. The SDK employs a batching technique to minimize
692-
the number of calls that are sent to Bucket's servers. During process shutdown,
693-
some messages could be waiting to be sent, and thus, would be discarded if the
694-
buffer is not flushed.
729+
BucketClient employs a batching technique to minimize the number of calls that are sent to
730+
Bucket's servers.
695731
696732
By default, the SDK automatically subscribes to process exit signals and attempts to flush
697733
any pending events. This behavior is controlled by the `flushOnExit` option in the client configuration:
@@ -704,17 +740,6 @@ const client = new BucketClient({
704740
});
705741
```
706742
707-
> [!NOTE]
708-
> If you are creating multiple client instances in your application, it's recommended to disable `flushOnExit`
709-
> to avoid potential conflicts during process shutdown. In such cases, you should implement your own flush handling.
710-
711-
When you bind a client to a user/company, this data is matched against the
712-
targeting rules. To get accurate targeting, you must ensure that the user/company
713-
information provided is sufficient to match against the targeting rules you've
714-
created. The user/company data is automatically transferred to Bucket. This ensures
715-
that you'll have up-to-date information about companies and users and accurate
716-
targeting information available in Bucket at all time.
717-
718743
## Tracking custom events and setting custom attributes
719744
720745
Tracking allows events and updating user/company attributes in Bucket.

packages/node-sdk/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
const base = require("@bucketco/eslint-config");
22

3-
module.exports = [...base, { ignores: ["dist/", "example/"] }];
3+
module.exports = [...base, { ignores: ["dist/", "examples/"] }];
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Logs
2+
3+
logs
4+
_.log
5+
npm-debug.log_
6+
yarn-debug.log*
7+
yarn-error.log*
8+
lerna-debug.log*
9+
.pnpm-debug.log*
10+
11+
# Diagnostic reports (https://nodejs.org/api/report.html)
12+
13+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14+
15+
# Runtime data
16+
17+
pids
18+
_.pid
19+
_.seed
20+
\*.pid.lock
21+
22+
# Directory for instrumented libs generated by jscoverage/JSCover
23+
24+
lib-cov
25+
26+
# Coverage directory used by tools like istanbul
27+
28+
coverage
29+
\*.lcov
30+
31+
# nyc test coverage
32+
33+
.nyc_output
34+
35+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36+
37+
.grunt
38+
39+
# Bower dependency directory (https://bower.io/)
40+
41+
bower_components
42+
43+
# node-waf configuration
44+
45+
.lock-wscript
46+
47+
# Compiled binary addons (https://nodejs.org/api/addons.html)
48+
49+
build/Release
50+
51+
# Dependency directories
52+
53+
node_modules/
54+
jspm_packages/
55+
56+
# Snowpack dependency directory (https://snowpack.dev/)
57+
58+
web_modules/
59+
60+
# TypeScript cache
61+
62+
\*.tsbuildinfo
63+
64+
# Optional npm cache directory
65+
66+
.npm
67+
68+
# Optional eslint cache
69+
70+
.eslintcache
71+
72+
# Optional stylelint cache
73+
74+
.stylelintcache
75+
76+
# Microbundle cache
77+
78+
.rpt2_cache/
79+
.rts2_cache_cjs/
80+
.rts2_cache_es/
81+
.rts2_cache_umd/
82+
83+
# Optional REPL history
84+
85+
.node_repl_history
86+
87+
# Output of 'npm pack'
88+
89+
\*.tgz
90+
91+
# Yarn Integrity file
92+
93+
.yarn-integrity
94+
95+
# dotenv environment variable files
96+
97+
.env
98+
.env.development.local
99+
.env.test.local
100+
.env.production.local
101+
.env.local
102+
103+
# parcel-bundler cache (https://parceljs.org/)
104+
105+
.cache
106+
.parcel-cache
107+
108+
# Next.js build output
109+
110+
.next
111+
out
112+
113+
# Nuxt.js build / generate output
114+
115+
.nuxt
116+
dist
117+
118+
# Gatsby files
119+
120+
.cache/
121+
122+
# Comment in the public line in if your project uses Gatsby and not Next.js
123+
124+
# https://nextjs.org/blog/next-9-1#public-directory-support
125+
126+
# public
127+
128+
# vuepress build output
129+
130+
.vuepress/dist
131+
132+
# vuepress v2.x temp and cache directory
133+
134+
.temp
135+
.cache
136+
137+
# Docusaurus cache and generated files
138+
139+
.docusaurus
140+
141+
# Serverless directories
142+
143+
.serverless/
144+
145+
# FuseBox cache
146+
147+
.fusebox/
148+
149+
# DynamoDB Local files
150+
151+
.dynamodb/
152+
153+
# TernJS port file
154+
155+
.tern-port
156+
157+
# Stores VSCode versions used for testing VSCode extensions
158+
159+
.vscode-test
160+
161+
# yarn v2
162+
163+
.yarn/cache
164+
.yarn/unplugged
165+
.yarn/build-state.yml
166+
.yarn/install-state.gz
167+
.pnp.\*
168+
169+
# wrangler project
170+
171+
.dev.vars
172+
.wrangler/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
worker-configuration.d.ts
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"files.associations": {
3+
"wrangler.json": "jsonc"
4+
}
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# cloudflare-worker
2+
3+
This is a simple example of how to use the Bucket SDK in a Cloudflare Worker.
4+
It demonstrates how to initialize the client and evaluate feature flags.
5+
It also shows how to flush the client and wait for any in-flight requests to complete.
6+
7+
- Set the BUCKET_SECRET_KEY environment variable in wrangler.jsonc to get started.
8+
- Run `yarn dev` in your terminal to start a development server
9+
- Open a browser tab at http://localhost:8787/ to see your worker in action
10+
- Run `yarn run deploy` to publish your worker
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "cloudflare-worker",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "wrangler deploy",
7+
"dev": "wrangler dev",
8+
"start": "wrangler dev",
9+
"test": "vitest",
10+
"cf-typegen": "wrangler types"
11+
},
12+
"devDependencies": {
13+
"@cloudflare/vitest-pool-workers": "^0.8.19",
14+
"typescript": "^5.5.2",
15+
"vitest": "~3.2.0",
16+
"wrangler": "^4.20.5"
17+
}
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* This is a simple example of how to use the Bucket SDK in a Cloudflare Worker.
3+
* It demonstrates how to initialize the client and evaluate feature flags.
4+
* It also shows how to flush the client and wait for any in-flight requests to complete.
5+
*
6+
* Set the BUCKET_SECRET_KEY environment variable in wrangler.jsonc to get started.
7+
*
8+
* - Run `yarn run dev` in your terminal to start a development server
9+
* - Open a browser tab at http://localhost:8787/ to see your worker in action
10+
* - Run `yarn run deploy` to publish your worker
11+
*
12+
*/
13+
14+
import { EdgeClient } from "../../../";
15+
16+
// set the BUCKET_SECRET_KEY environment variable or pass the secret key in the constructor
17+
const bucket = new EdgeClient();
18+
19+
export default {
20+
async fetch(request, _env, ctx): Promise<Response> {
21+
// initialize the client and wait for it to complete
22+
// this is not required for the edge client, but is included for completeness
23+
await bucket.initialize();
24+
25+
const url = new URL(request.url);
26+
const userId = url.searchParams.get("user.id");
27+
const companyId = url.searchParams.get("company.id");
28+
29+
const f = bucket.getFeatures({
30+
user: { id: userId ?? undefined },
31+
company: { id: companyId ?? undefined },
32+
});
33+
34+
// ensure all events are flushed and any requests to refresh the feature cache
35+
// have completed after the response is sent
36+
ctx.waitUntil(bucket.flush());
37+
38+
return new Response(
39+
`Features for user ${userId} and company ${companyId}: ${JSON.stringify(f, null, 2)}`,
40+
);
41+
},
42+
} satisfies ExportedHandler<Env>;

0 commit comments

Comments
 (0)