Skip to content

Commit 8c4f441

Browse files
committed
feat: add LaunchDarkly integration
1 parent 13ce909 commit 8c4f441

File tree

8 files changed

+257
-10
lines changed

8 files changed

+257
-10
lines changed

package-lock.json

+118
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
},
3434
"devDependencies": {
3535
"@types/react": "18.0.9",
36+
"launchdarkly-js-client-sdk": "3.1.0",
3637
"react": "18.1.0",
3738
"react-dom": "18.1.0",
3839
"rimraf": "3.0.2",
3940
"typescript": "4.9.4",
4041
"vitest": "0.12.6"
4142
},
4243
"peerDependencies": {
44+
"launchdarkly-js-client-sdk": "^3.0",
4345
"react": "^18.0 || ^17.0 || ^16.0",
4446
"react-dom": "^18.0 || ^17.0 || ^16.0"
4547
},
@@ -49,6 +51,9 @@
4951
},
5052
"react-dom": {
5153
"optional": true
54+
},
55+
"launchdarkly-js-client-sdk": {
56+
"optional": true
5257
}
5358
}
5459
}

src/core/keat.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ export function keatCore<TFeatures extends AnyFeatures>({
7474
if (!rules) return variates[variates.length - 1];
7575

7676
let result: unknown;
77-
let ctx: EvalCtx = { feature, variates, rules, user, configId };
77+
let ctx: EvalCtx = {
78+
feature,
79+
variates,
80+
rules,
81+
user,
82+
configId,
83+
};
7884

7985
const preApi: OnEvalApi = {
8086
setUser: (newUser) => {
@@ -84,11 +90,10 @@ export function keatCore<TFeatures extends AnyFeatures>({
8490

8591
plugins.forEach((p) => p.onPreEvaluate?.(ctx, preApi));
8692

87-
8893
for (let i = 0; i < variates.length; i++) {
8994
const variate = variates[i];
9095
const rule = rules[i];
91-
const ok = evaluateVariate(ctx, plugins, rule);
96+
const ok = evaluateVariate({ ...ctx, variate }, plugins, rule);
9297

9398
if (ok) {
9499
result = variate;
@@ -108,7 +113,10 @@ export function keatCore<TFeatures extends AnyFeatures>({
108113

109114
return {
110115
ready: (display: Display = defaultDisplay) => loader.ready(display),
111-
setUser: (user?: User) => (defaultUser = user),
116+
identify: (user?: User) => {
117+
defaultUser = user;
118+
plugins.forEach(p => p.onIdentify)
119+
},
112120
setDisplay: (display: Display) => (defaultDisplay = display),
113121
variation: <TFeature extends keyof TFeatures>(
114122
feature: TFeature,

src/core/plugin.ts

+18
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@ export type OnPluginInitApi = {
2828
onChange: () => void;
2929
};
3030

31+
export type OnPluginIdentifyHook = (
32+
ctx: OnPluginIdentifyCtx
33+
) => void | Promise<void>;
34+
35+
export type OnPluginIdentifyCtx = {
36+
user?: User;
37+
};
38+
3139
export type onPreEvaluateHook = (ctx: EvalCtx, api: OnEvalApi) => void;
3240

3341
export type EvalCtx = {
3442
feature: string;
43+
variate?: any;
3544
variates: any[];
3645
rules: Rule[];
3746
user: User | undefined;
@@ -57,6 +66,11 @@ export type Plugin<M extends Matcher = Matcher> = {
5766
*/
5867
onPluginInit?: OnPluginInitHook;
5968

69+
/**
70+
* Invoked when a user is identifier.
71+
*/
72+
onIdentify?: OnPluginIdentifyHook;
73+
6074
/**
6175
* Whether a literal matches this plugin.
6276
*/
@@ -93,6 +107,10 @@ export function createNopPlugin(): Plugin {
93107
};
94108
}
95109

110+
export const isAny: Matcher<any> = (literal) => {
111+
return literal;
112+
};
113+
96114
export const isBoolean: Matcher<boolean> = (literal) => {
97115
return typeof literal === "boolean" ? literal : null;
98116
};

src/core/types.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ export type IdentityFn = (user: User) => string;
2929
export type Display = "block" | "swap" | "fallback" | "optional";
3030

3131
export type Literal = boolean | string | number;
32-
export type Rule =
33-
| { OR: readonly Rule[] }
34-
| Literal;
32+
export type Rule = { OR: readonly Rule[] } | Literal;
3533

3634
export type Feature =
3735
| Rule
@@ -54,7 +52,7 @@ export type KeatInit<TFeatures extends AnyFeatures> = {
5452

5553
export type KeatApi<TFeatures extends AnyFeatures> = {
5654
ready(display?: Display): Promise<void>;
57-
setUser(user?: User): void;
55+
identify(user?: User): void;
5856
setDisplay(display: Display): void;
5957
variation<TFeature extends keyof TFeatures>(
6058
feature: TFeature,

src/plugins/launchdarkly.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
initialize,
3+
LDClient,
4+
LDContext,
5+
LDFlagChangeset,
6+
LDFlagSet,
7+
} from "launchdarkly-js-client-sdk";
8+
import { isAny, Plugin } from "../core/plugin";
9+
10+
type LaunchDarklyPluginOptions = {
11+
clientId: string;
12+
};
13+
14+
export const remoteConfig = (options: LaunchDarklyPluginOptions): Plugin => {
15+
let client: LDClient;
16+
let flags: LDFlagSet = {};
17+
18+
return {
19+
onPluginInit: async (_ctx, { onChange }) => {
20+
client = initialize(options.clientId, {
21+
anonymous: true,
22+
kind: "user",
23+
});
24+
25+
return new Promise<void>((r) => {
26+
function cleanup() {
27+
client.off("ready", handleReady);
28+
client.off("failed", handleFailure);
29+
}
30+
function handleFailure() {
31+
cleanup();
32+
r();
33+
}
34+
function handleReady() {
35+
cleanup();
36+
flags = client.allFlags();
37+
r();
38+
}
39+
client.on("failed", handleFailure);
40+
client.on("ready", handleReady);
41+
42+
client.on("change", (changes: LDFlagChangeset) => {
43+
for (const [flag, { current }] of Object.entries(changes)) {
44+
flags[flag] = current;
45+
}
46+
onChange();
47+
});
48+
});
49+
},
50+
onIdentify({ user }) {
51+
const context: LDContext = {
52+
kind: "user",
53+
key: user?.id ?? user?.sub ?? user?.email,
54+
...user,
55+
};
56+
client.identify(context);
57+
},
58+
matcher: isAny,
59+
evaluate({ feature, variate }) {
60+
return flags[feature] === variate;
61+
},
62+
};
63+
};

0 commit comments

Comments
 (0)