-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathvariant.ts
136 lines (119 loc) · 3.31 KB
/
variant.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { Context } from './context';
// eslint-disable-next-line import/no-cycle
import { FeatureInterface } from './feature';
import { normalizedVariantValue } from './strategy/util';
import { resolveContextValue } from './helpers';
export enum PayloadType {
STRING = 'string',
JSON = 'json',
CSV = 'csv',
NUMBER = 'number',
}
interface Override {
contextName: string;
values: string[];
}
export interface Payload {
type: PayloadType;
value: string;
}
export interface VariantDefinition {
name: string;
weight: number;
stickiness?: string;
payload?: Payload;
overrides?: Override[];
}
export interface Variant {
name: string;
enabled: boolean;
payload?: Payload;
feature_enabled?: boolean;
}
export const defaultVariant: Variant = {
name: 'disabled',
enabled: false,
feature_enabled: false,
};
/**
* @deprecated Use {@link Variant} instead and feature_enabled field
*/
export interface VariantWithFeatureStatus extends Variant {
/**
* @deprecated Use {@link feature_enabled} instead
*/
featureEnabled: boolean;
}
/**
* @deprecated Use {@link defaultVariant} const instead
*/
export function getDefaultVariant(): Variant {
return defaultVariant;
}
function randomString() {
return String(Math.round(Math.random() * 100000));
}
const stickinessSelectors = ['userId', 'sessionId', 'remoteAddress'];
function getSeed(context: Context, stickiness: string = 'default'): string {
if (stickiness !== 'default') {
const value = resolveContextValue(context, stickiness);
return value ? value.toString() : randomString();
}
let result;
stickinessSelectors.some((key: string): boolean => {
const value = context[key];
if (typeof value === 'string' && value !== '') {
result = value;
return true;
}
return false;
});
return result || randomString();
}
function overrideMatchesContext(context: Context): (o: Override) => boolean {
return (o: Override) =>
o.values.some((value) => value === resolveContextValue(context, o.contextName));
}
function findOverride(
variants: VariantDefinition[],
context: Context,
): VariantDefinition | undefined {
return variants
.filter((variant) => variant.overrides)
.find((variant) => variant.overrides?.some(overrideMatchesContext(context)));
}
export function selectVariantDefinition(
groupId: string,
stickiness: string | undefined,
variants: VariantDefinition[],
context: Context,
): VariantDefinition | null {
const totalWeight = variants.reduce((acc, v) => acc + v.weight, 0);
if (totalWeight <= 0) {
return null;
}
const variantOverride = findOverride(variants, context);
if (variantOverride) {
return variantOverride;
}
const target = normalizedVariantValue(getSeed(context, stickiness), groupId, totalWeight);
let counter = 0;
const variant = variants.find((v: VariantDefinition): VariantDefinition | undefined => {
if (v.weight === 0) {
return undefined;
}
counter += v.weight;
if (counter < target) {
return undefined;
}
return v;
});
return variant || null;
}
export function selectVariant(
feature: FeatureInterface,
context: Context,
): VariantDefinition | null {
const stickiness = feature.variants?.[0]?.stickiness ?? undefined;
return selectVariantDefinition(feature.name, stickiness, feature.variants || [], context);
}