Skip to content

Commit 5f95e4f

Browse files
committed
Merge branch 'main' into update-node-sdk
2 parents d3bf876 + 5756dc1 commit 5f95e4f

4 files changed

Lines changed: 40 additions & 60 deletions

File tree

packages/flag-evaluation/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/flag-evaluation",
3-
"version": "0.2.0",
3+
"version": "0.1.2",
44
"license": "MIT",
55
"repository": {
66
"type": "git",
@@ -33,5 +33,8 @@
3333
"ts-node": "^10.9.2",
3434
"typescript": "^5.7.3",
3535
"vitest": "^2.0.5"
36+
},
37+
"dependencies": {
38+
"js-sha256": "0.11.0"
3639
}
3740
}

packages/flag-evaluation/src/index.ts

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
try {
2-
// crypto not available on globalThis in Node.js v18
3-
// eslint-disable-next-line @typescript-eslint/no-require-imports
4-
globalThis.crypto ??= require("node:crypto").webcrypto;
5-
} catch {
6-
// ignore
7-
}
1+
import { sha256 } from "js-sha256";
82

93
/**
104
* Represents a filter class with a specific type property.
@@ -265,19 +259,18 @@ export function unflattenJSON(data: Record<string, any>): Record<string, any> {
265259
* @param {string} hashInput - The input string used to generate the hash.
266260
* @return {number} A number between 0 and 100000 derived from the hash of the input string.
267261
*/
268-
export async function hashInt(hashInput: string): Promise<number> {
262+
export function hashInt(hashInput: string): number {
269263
// 1. hash the key and the partial rollout attribute
270264
// 2. take 20 bits from the hash and divide by 2^20 - 1 to get a number between 0 and 1
271265
// 3. multiply by 100000 to get a number between 0 and 100000 and compare it to the threshold
272266
//
273267
// we only need 20 bits to get to 100000 because 2^20 is 1048576
274-
const msgUint8 = new TextEncoder().encode(hashInput);
275-
276-
// Hash the message
277-
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
268+
const value =
269+
new DataView(sha256.create().update(hashInput).arrayBuffer()).getUint32(
270+
0,
271+
true,
272+
) & 0xfffff;
278273

279-
const view = new DataView(hashBuffer);
280-
const value = view.getUint32(0, true) & 0xfffff;
281274
return Math.floor((value / 0xfffff) * 100000);
282275
}
283276

@@ -351,11 +344,11 @@ export function evaluate(
351344
}
352345
}
353346

354-
async function evaluateRecursively(
347+
function evaluateRecursively(
355348
filter: RuleFilter,
356349
context: Record<string, string>,
357350
missingContextFieldsSet: Set<string>,
358-
): Promise<boolean> {
351+
): boolean {
359352
switch (filter.type) {
360353
case "constant":
361354
return filter.value;
@@ -376,38 +369,30 @@ async function evaluateRecursively(
376369
return false;
377370
}
378371

379-
const hashVal = await hashInt(
372+
const hashVal = hashInt(
380373
`${filter.key}.${context[filter.partialRolloutAttribute]}`,
381374
);
382375

383376
return hashVal < filter.partialRolloutThreshold;
384377
}
385-
case "group": {
386-
const isAnd = filter.operator === "and";
387-
let result = isAnd;
388-
for (const current of filter.filters) {
389-
// short-circuit if we know the result already
390-
// could be simplified to isAnd !== result, but this is more readable
391-
if ((isAnd && !result) || (!isAnd && result)) {
392-
return result;
378+
case "group":
379+
return filter.filters.reduce((acc, current) => {
380+
if (filter.operator === "and") {
381+
return (
382+
acc &&
383+
evaluateRecursively(current, context, missingContextFieldsSet)
384+
);
393385
}
394-
395-
const newRes = await evaluateRecursively(
396-
current,
397-
context,
398-
missingContextFieldsSet,
386+
return (
387+
acc || evaluateRecursively(current, context, missingContextFieldsSet)
399388
);
400-
401-
result = isAnd ? result && newRes : result || newRes;
402-
}
403-
return result;
404-
}
389+
}, filter.operator === "and");
405390
case "negation":
406-
return !(await evaluateRecursively(
391+
return !evaluateRecursively(
407392
filter.filter,
408393
context,
409394
missingContextFieldsSet,
410-
));
395+
);
411396
default:
412397
return false;
413398
}
@@ -449,18 +434,16 @@ export interface EvaluationResult<T extends RuleValue> {
449434
missingContextFields?: string[];
450435
}
451436

452-
export async function evaluateFeatureRules<T extends RuleValue>({
437+
export function evaluateFeatureRules<T extends RuleValue>({
453438
context,
454439
featureKey,
455440
rules,
456-
}: EvaluationParams<T>): Promise<EvaluationResult<T>> {
441+
}: EvaluationParams<T>): EvaluationResult<T> {
457442
const flatContext = flattenJSON(context);
458443
const missingContextFieldsSet = new Set<string>();
459444

460-
const ruleEvaluationResults = await Promise.all(
461-
rules.map((rule) =>
462-
evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet),
463-
),
445+
const ruleEvaluationResults = rules.map((rule) =>
446+
evaluateRecursively(rule.filter, flatContext, missingContextFieldsSet),
464447
);
465448

466449
const missingContextFields = Array.from(missingContextFieldsSet);

packages/flag-evaluation/test/index.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const feature = {
3838

3939
describe("evaluate feature targeting integration ", () => {
4040
it("evaluates all kinds of filters", async () => {
41-
const res = await evaluateFeatureRules({
41+
const res = evaluateFeatureRules({
4242
featureKey: "feature",
4343
rules: [
4444
{
@@ -109,7 +109,7 @@ describe("evaluate feature targeting integration ", () => {
109109
});
110110

111111
it("evaluates flag when there's no matching rule", async () => {
112-
const res = await evaluateFeatureRules({
112+
const res = evaluateFeatureRules({
113113
...feature,
114114
context: {
115115
company: {
@@ -137,7 +137,7 @@ describe("evaluate feature targeting integration ", () => {
137137
},
138138
};
139139

140-
const res = await evaluateFeatureRules({
140+
const res = evaluateFeatureRules({
141141
...feature,
142142
context,
143143
});
@@ -155,7 +155,7 @@ describe("evaluate feature targeting integration ", () => {
155155
});
156156

157157
it("evaluates flag with missing values", async () => {
158-
const res = await evaluateFeatureRules({
158+
const res = evaluateFeatureRules({
159159
featureKey: "feature",
160160
rules: [
161161
{
@@ -198,7 +198,7 @@ describe("evaluate feature targeting integration ", () => {
198198
});
199199

200200
it("returns list of missing context keys ", async () => {
201-
const res = await evaluateFeatureRules({
201+
const res = evaluateFeatureRules({
202202
...feature,
203203
context: {},
204204
});
@@ -214,7 +214,7 @@ describe("evaluate feature targeting integration ", () => {
214214
});
215215

216216
it("fails evaluation and includes key in missing keys when rollout attribute is missing from context", async () => {
217-
const res = await evaluateFeatureRules({
217+
const res = evaluateFeatureRules({
218218
featureKey: "myfeature",
219219
rules: [
220220
{
@@ -352,8 +352,8 @@ describe("rollout hash", () => {
352352
] as const;
353353

354354
for (const [input, expected] of tests) {
355-
it(`evaluates '${input}' = ${expected}`, async () => {
356-
const res = await hashInt(input);
355+
it(`evaluates '${input}' = ${expected}`, () => {
356+
const res = hashInt(input);
357357
expect(res).toEqual(expected);
358358
});
359359
}

yarn.lock

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -554,21 +554,15 @@ __metadata:
554554
languageName: node
555555
linkType: hard
556556

557-
"@bucketco/flag-evaluation@npm:~0.1.0":
558-
version: 0.1.0
559-
resolution: "@bucketco/flag-evaluation@npm:0.1.0"
560-
checksum: 10c0/a5d747962d43ce12b194735e92524a576edac9e9ad53c425b4a517123ca9918d3001891cd212f178b7cf6b235c79aa5cfa3942e162187da056c2ae1d5230a984
561-
languageName: node
562-
linkType: hard
563-
564-
"@bucketco/flag-evaluation@workspace:packages/flag-evaluation":
557+
"@bucketco/flag-evaluation@npm:~0.1.0, @bucketco/flag-evaluation@workspace:packages/flag-evaluation":
565558
version: 0.0.0-use.local
566559
resolution: "@bucketco/flag-evaluation@workspace:packages/flag-evaluation"
567560
dependencies:
568561
"@bucketco/eslint-config": "workspace:^"
569562
"@bucketco/tsconfig": "workspace:^"
570563
"@types/node": "npm:^22.12.0"
571564
eslint: "npm:^9.21.0"
565+
js-sha256: "npm:0.11.0"
572566
prettier: "npm:^3.5.2"
573567
ts-node: "npm:^10.9.2"
574568
typescript: "npm:^5.7.3"

0 commit comments

Comments
 (0)