Skip to content

Commit 3bf5005

Browse files
committed
extract @flags-sdk/utils
1 parent 735a6ee commit 3bf5005

File tree

5 files changed

+92
-40
lines changed

5 files changed

+92
-40
lines changed

packages/adapter-launchdarkly/src/index.ts

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {
33
type LDClient,
44
type LDContext,
55
} from '@launchdarkly/vercel-server-sdk';
6-
import { createClient, type EdgeConfigClient } from '@vercel/edge-config';
7-
import { AsyncLocalStorage } from 'async_hooks';
6+
import { createClient } from '@vercel/edge-config';
87
import type { Adapter } from 'flags';
8+
import { createCachedEdgeConfigClient as cacheEdgeConfigClient } from 'flags/utils';
99

1010
export { getProviderData } from './provider';
1111
export type { LDContext };
@@ -46,31 +46,10 @@ export function createLaunchDarklyAdapter({
4646
edgeConfigConnectionString: string;
4747
}): AdapterResponse {
4848
const edgeConfigClient = createClient(edgeConfigConnectionString);
49-
50-
const store = new AsyncLocalStorage<WeakKey>();
51-
const cache = new WeakMap<WeakKey, Promise<unknown>>();
52-
53-
const patchedEdgeConfigClient: EdgeConfigClient = {
54-
...edgeConfigClient,
55-
get: async <T>(key: string) => {
56-
const h = store.getStore();
57-
if (h) {
58-
const cached = cache.get(h);
59-
if (cached) {
60-
return cached as Promise<T>;
61-
}
62-
}
63-
64-
const promise = edgeConfigClient.get<T>(key);
65-
if (h) cache.set(h, promise);
66-
67-
return promise;
68-
},
69-
};
49+
const { run, client } = cacheEdgeConfigClient(edgeConfigClient);
7050

7151
let initPromise: Promise<unknown> | null = null;
72-
73-
const ldClient = init(clientSideId, patchedEdgeConfigClient);
52+
const ldClient = init(clientSideId, client);
7453

7554
function origin(key: string) {
7655
return `https://app.launchdarkly.com/projects/${projectSlug}/flags/${key}/`;
@@ -87,7 +66,7 @@ export function createLaunchDarklyAdapter({
8766
await initPromise;
8867
}
8968

90-
return store.run(
69+
return run(
9170
headers,
9271
() =>
9372
ldClient.variation(

packages/flags/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
"import": "./dist/index.js",
2727
"require": "./dist/index.cjs"
2828
},
29+
"./utils": {
30+
"import": "./dist/utils.js",
31+
"require": "./dist/utils.cjs"
32+
},
2933
"./next": {
3034
"import": "./dist/next.js",
3135
"require": "./dist/next.cjs"
@@ -50,6 +54,10 @@
5054
"dist/*.d.ts",
5155
"dist/*.d.cts"
5256
],
57+
"utils": [
58+
"dist/utils.d.ts",
59+
"dist/utils.d.cts"
60+
],
5361
"next": [
5462
"dist/next.d.ts",
5563
"dist/next.d.cts"
@@ -79,6 +87,7 @@
7987
},
8088
"dependencies": {
8189
"@edge-runtime/cookies": "^5.0.2",
90+
"@vercel/edge-config": "^1.4.0",
8291
"jose": "^5.2.1"
8392
},
8493
"devDependencies": {

packages/flags/src/utils.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** This file is an entry point for flags/utils, so its exports are public */
2+
import type { EdgeConfigClient } from '@vercel/edge-config';
3+
import { AsyncLocalStorage } from 'async_hooks';
4+
5+
/**
6+
* Returns a patched version of the EdgeConfigClient that cache reads
7+
* for the duration of a request.
8+
*
9+
* Uses the headers as a cache key.
10+
*/
11+
export function createCachedEdgeConfigClient(
12+
edgeConfigClient: EdgeConfigClient,
13+
): {
14+
/**
15+
* A patched version of the Edge Config client, which only
16+
* reads Edge Config once per request and caches the result
17+
* for the duration of the request.
18+
*/
19+
client: EdgeConfigClient;
20+
run: <R>(headers: HeadersInit, fn: () => R) => R;
21+
} {
22+
const store = new AsyncLocalStorage<WeakKey>();
23+
const cache = new WeakMap<WeakKey, Promise<unknown>>();
24+
25+
const patchedEdgeConfigClient: EdgeConfigClient = {
26+
...edgeConfigClient,
27+
get: async <T>(key: string) => {
28+
const h = store.getStore();
29+
if (h) {
30+
const cached = cache.get(h);
31+
if (cached) {
32+
return cached as Promise<T>;
33+
}
34+
}
35+
36+
const promise = edgeConfigClient.get<T>(key);
37+
if (h) cache.set(h, promise);
38+
39+
return promise;
40+
},
41+
};
42+
43+
return {
44+
client: patchedEdgeConfigClient,
45+
// The "run" function puts the headers into the AsyncLocalStorage, which
46+
// allows the patchedEdgeConfigClient to read the headers without otherwise
47+
// having access to them.
48+
//
49+
// The patchedEdgeConfigClient then uses the headers as a cache key to
50+
// deduplicate Edge Config reads for the duration of a request.
51+
//
52+
// Performance wise it would actually be fine to read Edge Config on every
53+
// flag evaluation, but this would also cause a lot of unnecessary reads,
54+
// and as Edge Config is charged per read, this would cause unnecessary charges.
55+
//
56+
// So this approach helps with performance and ensures a fair usage of Edge Config.
57+
run: store.run.bind(store),
58+
};
59+
}

packages/flags/tsup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const defaultConfig = {
1414
export default defineConfig({
1515
entry: {
1616
index: 'src/index.ts',
17+
utils: 'src/utils.ts',
1718
next: 'src/next/index.ts',
1819
sveltekit: 'src/sveltekit/index.ts',
1920
react: 'src/react/index.tsx',

pnpm-lock.yaml

Lines changed: 18 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)