@vercel/flags-core 0.1.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,566 +0,0 @@
1
- // src/index.ts
2
- import { createClient as createEdgeConfigClient } from "@vercel/edge-config";
3
-
4
- // src/evaluate.ts
5
- import { xxHash32 as hashInput } from "js-xxhash";
6
-
7
- // src/types.ts
8
- var ResolutionReason = /* @__PURE__ */ ((ResolutionReason2) => {
9
- ResolutionReason2["PAUSED"] = "paused";
10
- ResolutionReason2["TARGET_MATCH"] = "target_match";
11
- ResolutionReason2["RULE_MATCH"] = "rule_match";
12
- ResolutionReason2["FALLTHROUGH"] = "fallthrough";
13
- ResolutionReason2["ERROR"] = "error";
14
- return ResolutionReason2;
15
- })(ResolutionReason || {});
16
- var Original;
17
- ((Original2) => {
18
- let AccessorType;
19
- ((AccessorType2) => {
20
- AccessorType2["SEGMENT"] = "segment";
21
- AccessorType2["ENTITY"] = "entity";
22
- })(AccessorType = Original2.AccessorType || (Original2.AccessorType = {}));
23
- })(Original || (Original = {}));
24
- var Packed;
25
- ((Packed2) => {
26
- let AccessorType;
27
- ((AccessorType2) => {
28
- AccessorType2["SEGMENT"] = "segment";
29
- AccessorType2["ENTITY"] = "entity";
30
- })(AccessorType = Packed2.AccessorType || (Packed2.AccessorType = {}));
31
- })(Packed || (Packed = {}));
32
-
33
- // src/utils.ts
34
- function exhaustivenessCheck(_) {
35
- throw new Error("Exhaustiveness check failed");
36
- }
37
-
38
- // src/evaluate.ts
39
- function getProperty(obj, pathArray) {
40
- return pathArray.reduce((acc, key) => {
41
- if (acc && key in acc) {
42
- return acc[key];
43
- }
44
- return void 0;
45
- }, obj);
46
- }
47
- function access(lhs, params) {
48
- if (Array.isArray(lhs))
49
- return getProperty(params.entities, lhs);
50
- if (lhs === Packed.AccessorType.SEGMENT)
51
- throw new Error("Unexpected segment");
52
- throw new Error("Accessor not implemented");
53
- }
54
- function isString(input) {
55
- return typeof input === "string";
56
- }
57
- function isNumber(input) {
58
- return typeof input === "number";
59
- }
60
- function isArray(input) {
61
- return Array.isArray(input);
62
- }
63
- function matchTargetList(targets, params) {
64
- for (const [kind, attributes] of Object.entries(targets)) {
65
- for (const [attribute, values] of Object.entries(attributes)) {
66
- const entity = access([kind, attribute], params);
67
- if (isString(entity) && values.includes(entity))
68
- return true;
69
- }
70
- }
71
- return false;
72
- }
73
- function matchSegment(segment, params) {
74
- var _a;
75
- if (segment.include && matchTargetList(segment.include, params))
76
- return true;
77
- if (segment.exclude && matchTargetList(segment.exclude, params))
78
- return false;
79
- if (!((_a = segment.rules) == null ? void 0 : _a.length))
80
- return false;
81
- const firstMatchingRule = segment.rules.find(
82
- (rule) => matchConditions(rule.conditions, params)
83
- );
84
- if (!firstMatchingRule)
85
- return false;
86
- return handleSegmentOutcome(params, firstMatchingRule.outcome);
87
- }
88
- function matchSegmentCondition(cmp, rhs, params) {
89
- var _a, _b;
90
- switch (cmp) {
91
- case "eq" /* EQ */: {
92
- const segment = (_a = params.segments) == null ? void 0 : _a[rhs];
93
- if (!segment)
94
- return false;
95
- return matchSegment(segment, params);
96
- }
97
- case "!eq" /* NOT_EQ */: {
98
- const segment = (_b = params.segments) == null ? void 0 : _b[rhs];
99
- if (!segment)
100
- return false;
101
- return !matchSegment(segment, params);
102
- }
103
- case "oneOf" /* ONE_OF */: {
104
- if (!isArray(rhs))
105
- return false;
106
- const segmentIds = rhs;
107
- return segmentIds.some((segmentId) => {
108
- var _a2;
109
- const segment = (_a2 = params.segments) == null ? void 0 : _a2[segmentId];
110
- if (!segment)
111
- return false;
112
- return matchSegment(segment, params);
113
- });
114
- }
115
- case "!oneOf" /* NOT_ONE_OF */: {
116
- const segmentIds = rhs;
117
- return segmentIds.every((segmentId) => {
118
- var _a2;
119
- const segment = (_a2 = params.segments) == null ? void 0 : _a2[segmentId];
120
- if (!segment)
121
- return false;
122
- return matchSegment(segment, params);
123
- });
124
- }
125
- default:
126
- throw new Error(`Comparator ${cmp} not implemented for segment`);
127
- }
128
- }
129
- function matchConditions(conditions, params) {
130
- return conditions.every((condition) => {
131
- const [lhsAccessor, cmpKey, rhs] = condition;
132
- if (lhsAccessor === Packed.AccessorType.SEGMENT) {
133
- return rhs && matchSegmentCondition(cmpKey, rhs, params);
134
- }
135
- const lhs = access(lhsAccessor, params);
136
- try {
137
- switch (cmpKey) {
138
- case "eq" /* EQ */:
139
- return lhs === rhs;
140
- case "!eq" /* NOT_EQ */:
141
- return lhs !== rhs;
142
- case "oneOf" /* ONE_OF */:
143
- return isArray(rhs) && rhs.includes(lhs);
144
- case "!oneOf" /* NOT_ONE_OF */:
145
- return isArray(rhs) && typeof lhs !== "undefined" && !rhs.includes(lhs);
146
- case "containsAllOf" /* CONTAINS_ALL_OF */: {
147
- if (!Array.isArray(rhs) || !Array.isArray(lhs))
148
- return false;
149
- const lhsSet = new Set(lhs.filter(isString));
150
- if (lhsSet.size === lhs.length) {
151
- return rhs.filter(isString).every((item) => lhsSet.has(item));
152
- }
153
- return rhs.every((item) => lhs.includes(item));
154
- }
155
- case "containsAnyOf" /* CONTAINS_ANY_OF */: {
156
- if (!Array.isArray(rhs) || !Array.isArray(lhs))
157
- return false;
158
- const rhsSet = new Set(rhs.filter(isString));
159
- return lhs.some(
160
- rhsSet.size === rhs.length ? (
161
- // try to use a set if the rhs is a list of strings - O(1)
162
- (item) => rhsSet.has(item)
163
- ) : (
164
- // otherwise we need to iterate over the values - O(n)
165
- (item) => rhs.includes(item)
166
- )
167
- );
168
- }
169
- case "containsNoneOf" /* CONTAINS_NONE_OF */: {
170
- if (!Array.isArray(rhs))
171
- return false;
172
- if (!Array.isArray(lhs))
173
- return true;
174
- const rhsSet = new Set(rhs.filter(isString));
175
- return lhs.every(
176
- rhsSet.size === rhs.length ? (
177
- // try to use a set if the rhs is a list of strings - O(1)
178
- (item) => !rhsSet.has(item)
179
- ) : (
180
- // otherwise we need to iterate over the values - O(n)
181
- (item) => !rhs.includes(item)
182
- )
183
- );
184
- }
185
- case "startsWith" /* STARTS_WITH */:
186
- return isString(lhs) && isString(rhs) && lhs.startsWith(rhs);
187
- case "!startsWith" /* NOT_STARTS_WITH */:
188
- return isString(lhs) && isString(rhs) && !lhs.startsWith(rhs);
189
- case "endsWith" /* ENDS_WITH */:
190
- return isString(lhs) && isString(rhs) && lhs.endsWith(rhs);
191
- case "!endsWith" /* NOT_ENDS_WITH */:
192
- return isString(lhs) && isString(rhs) && !lhs.endsWith(rhs);
193
- case "ex" /* EXISTS */:
194
- return lhs !== void 0 && lhs !== null;
195
- case "!ex" /* NOT_EXISTS */:
196
- return lhs === void 0 || lhs === null;
197
- case "gt" /* GT */:
198
- if (lhs === null || lhs === void 0)
199
- return false;
200
- return (isNumber(rhs) || isString(rhs)) && lhs > rhs;
201
- case "gte" /* GTE */:
202
- if (lhs === null || lhs === void 0)
203
- return false;
204
- return (isNumber(rhs) || isString(rhs)) && lhs >= rhs;
205
- case "lt" /* LT */:
206
- if (lhs === null || lhs === void 0)
207
- return false;
208
- return (isNumber(rhs) || isString(rhs)) && lhs < rhs;
209
- case "lte" /* LTE */:
210
- if (lhs === null || lhs === void 0)
211
- return false;
212
- return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
213
- case "regex" /* REGEX */:
214
- if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && (rhs == null ? void 0 : rhs.type) === "regex") {
215
- return new RegExp(rhs.pattern, rhs.flags).test(lhs);
216
- }
217
- return false;
218
- case "!regex" /* NOT_REGEX */:
219
- if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && (rhs == null ? void 0 : rhs.type) === "regex") {
220
- return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
221
- }
222
- return false;
223
- case "before" /* BEFORE */: {
224
- if (!isString(lhs) || !isString(rhs))
225
- return false;
226
- const a = new Date(lhs);
227
- const b = new Date(rhs);
228
- return a.getTime() < b.getTime();
229
- }
230
- case "after" /* AFTER */: {
231
- if (!isString(lhs) || !isString(rhs))
232
- return false;
233
- const a = new Date(lhs);
234
- const b = new Date(rhs);
235
- return a.getTime() > b.getTime();
236
- }
237
- default: {
238
- const _x = cmpKey;
239
- return false;
240
- }
241
- }
242
- } catch (error) {
243
- console.error("flags: Error matching conditions", error);
244
- return false;
245
- }
246
- });
247
- }
248
- function sum(list) {
249
- return list.reduce((acc, n) => acc + n, 0);
250
- }
251
- function handleSegmentOutcome(params, outcome) {
252
- if (outcome === 1)
253
- return true;
254
- switch (outcome.type) {
255
- case "split": {
256
- const lhs = access(outcome.base, params);
257
- if (typeof lhs !== "string")
258
- return false;
259
- const maxValue = 1e5;
260
- if (outcome.passPromille <= 0)
261
- return false;
262
- if (outcome.passPromille >= maxValue)
263
- return true;
264
- const value = hashInput(lhs, params.definition.seed) % maxValue;
265
- return value < outcome.passPromille;
266
- }
267
- default: {
268
- const { type } = outcome;
269
- exhaustivenessCheck(type);
270
- }
271
- }
272
- }
273
- function handleOutcome(params, outcome) {
274
- if (typeof outcome === "number") {
275
- return {
276
- value: params.definition.variants[outcome],
277
- outcomeType: "value" /* VALUE */
278
- };
279
- }
280
- switch (outcome.type) {
281
- case "split": {
282
- const lhs = access(outcome.base, params);
283
- const defaultOutcome = params.definition.variants[outcome.defaultVariant];
284
- if (typeof lhs !== "string") {
285
- return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
286
- }
287
- const maxValue = 4294967295;
288
- const value = hashInput(lhs, params.definition.seed);
289
- const sumOfWeights = sum(outcome.weights);
290
- const scaledWeights = outcome.weights.map(
291
- (weight) => weight / sumOfWeights * maxValue
292
- );
293
- const variantIndex = findWeightedIndex(scaledWeights, value, maxValue);
294
- return {
295
- value: variantIndex === -1 ? defaultOutcome : params.definition.variants[variantIndex],
296
- outcomeType: "split" /* SPLIT */
297
- };
298
- }
299
- default: {
300
- const { type } = outcome;
301
- exhaustivenessCheck(type);
302
- }
303
- }
304
- }
305
- function evaluate(params) {
306
- const envConfig = params.definition.environments[params.environment];
307
- if (typeof envConfig === "number") {
308
- return {
309
- ...handleOutcome(params, envConfig),
310
- reason: "paused" /* PAUSED */
311
- };
312
- }
313
- if (!envConfig) {
314
- return {
315
- reason: "error" /* ERROR */,
316
- errorMessage: `Could not find envConfig for "${params.environment}"`,
317
- value: params.defaultValue
318
- };
319
- }
320
- if ("reuse" in envConfig) {
321
- const reuseEnvConfig = params.definition.environments[envConfig.reuse];
322
- if (reuseEnvConfig === void 0) {
323
- throw new Error(
324
- `Could not find envConfig for "${envConfig.reuse}" when reusing`
325
- );
326
- }
327
- return evaluate({ ...params, environment: envConfig.reuse });
328
- }
329
- if (envConfig.targets) {
330
- const matchedIndex = envConfig.targets.findIndex(
331
- (targetList) => matchTargetList(targetList, params)
332
- );
333
- if (matchedIndex > -1) {
334
- return {
335
- ...handleOutcome(params, matchedIndex),
336
- reason: "target_match" /* TARGET_MATCH */
337
- };
338
- }
339
- }
340
- const firstMatchingRule = envConfig.rules ? envConfig.rules.find((rule) => matchConditions(rule.conditions, params)) : void 0;
341
- if (firstMatchingRule) {
342
- return {
343
- ...handleOutcome(params, firstMatchingRule.outcome),
344
- reason: "rule_match" /* RULE_MATCH */
345
- };
346
- }
347
- return {
348
- ...handleOutcome(params, envConfig.fallthrough),
349
- reason: "fallthrough" /* FALLTHROUGH */
350
- };
351
- }
352
- function findWeightedIndex(weights, value, maxValue) {
353
- if (value < 0 || value >= maxValue)
354
- return -1;
355
- let sum2 = 0;
356
- for (let i = 0; i < weights.length; i++) {
357
- sum2 += weights[i];
358
- if (value < sum2)
359
- return i;
360
- }
361
- return -1;
362
- }
363
-
364
- // package.json
365
- var version = "0.1.8";
366
-
367
- // src/lib/report-value.ts
368
- function internalReportValue(key, value, data) {
369
- var _a, _b;
370
- const symbol = Symbol.for("@vercel/request-context");
371
- const ctx = (_a = Reflect.get(globalThis, symbol)) == null ? void 0 : _a.get();
372
- (_b = ctx == null ? void 0 : ctx.flags) == null ? void 0 : _b.reportValue(key, value, {
373
- sdkVersion: version,
374
- ...data
375
- });
376
- }
377
-
378
- // src/client.ts
379
- function createClient({
380
- environment,
381
- dataSource
382
- }) {
383
- return {
384
- dataSource,
385
- environment,
386
- initialize: () => {
387
- if (dataSource && typeof dataSource.initialize === "function") {
388
- return dataSource.initialize();
389
- }
390
- },
391
- shutdown: () => {
392
- if (dataSource && typeof dataSource.shutdown === "function") {
393
- return dataSource.shutdown();
394
- }
395
- },
396
- async evaluate(flagKey, defaultValue, entities) {
397
- const data = await dataSource.getData();
398
- const flagDefinition = data.definitions[flagKey];
399
- if (flagDefinition === void 0) {
400
- return {
401
- value: defaultValue,
402
- reason: "error" /* ERROR */,
403
- errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
404
- errorMessage: `Definition not found for flag "${flagKey}"`
405
- };
406
- }
407
- const result = evaluate({
408
- defaultValue,
409
- definition: flagDefinition,
410
- environment: this.environment,
411
- entities: entities ?? {},
412
- segments: data.segments
413
- });
414
- if (dataSource.projectId) {
415
- internalReportValue(flagKey, result.value, {
416
- originProjectId: dataSource.projectId,
417
- originProvider: "vercel",
418
- reason: result.reason,
419
- outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
420
- });
421
- }
422
- return result;
423
- }
424
- };
425
- }
426
-
427
- // src/store.ts
428
- import { AsyncLocalStorage } from "node:async_hooks";
429
- var store = new AsyncLocalStorage();
430
-
431
- // src/data-source/edge-config-data-source.ts
432
- var EdgeConfigDataSource = class {
433
- connectionString;
434
- edgeConfigClient;
435
- edgeConfigItemKey;
436
- requestCache;
437
- projectId;
438
- constructor(options) {
439
- this.edgeConfigClient = options.edgeConfigClient;
440
- this.edgeConfigItemKey = options.edgeConfigItemKey;
441
- this.requestCache = /* @__PURE__ */ new WeakMap();
442
- this.projectId = options.projectId;
443
- }
444
- // This is a temporary solution to avoid reading the Edge Config for every flag,
445
- // and instead reading it once per request.
446
- async getCachedData() {
447
- const cacheKey = store.getStore();
448
- if (cacheKey) {
449
- const cached = this.requestCache.get(cacheKey);
450
- if (cached) {
451
- return cached;
452
- }
453
- }
454
- const promise = this.edgeConfigClient.get(
455
- this.edgeConfigItemKey
456
- );
457
- if (cacheKey)
458
- this.requestCache.set(cacheKey, promise);
459
- return promise;
460
- }
461
- async getData() {
462
- const data = await this.getCachedData();
463
- if (!data) {
464
- throw new Error(
465
- `No definitions found in Edge Config under key "${this.edgeConfigItemKey}"`
466
- );
467
- }
468
- return data;
469
- }
470
- };
471
-
472
- // src/data-source/in-memory-data-source.ts
473
- var InMemoryDataSource = class {
474
- data;
475
- projectId;
476
- constructor(data, projectId) {
477
- this.data = data;
478
- this.projectId = projectId;
479
- }
480
- async getData() {
481
- return this.data;
482
- }
483
- };
484
-
485
- // src/index.ts
486
- var defaultFlagsClient = null;
487
- function parseFlagsConnectionString(connectionString) {
488
- const errorMessage = "flags: Invalid connection string";
489
- try {
490
- const params = new URLSearchParams(connectionString.slice(6));
491
- const edgeConfigId = params.get("edgeConfigId");
492
- const edgeConfigToken = params.get("edgeConfigToken");
493
- const projectId = params.get("projectId");
494
- if (!edgeConfigId || !edgeConfigToken || !projectId) {
495
- throw new Error(errorMessage);
496
- }
497
- return {
498
- edgeConfigId,
499
- edgeConfigToken,
500
- projectId,
501
- edgeConfigItemKey: params.get("edgeConfigItemKey"),
502
- env: params.get("env")
503
- };
504
- } catch {
505
- throw new Error(errorMessage);
506
- }
507
- }
508
- function resetDefaultFlagsClient() {
509
- defaultFlagsClient = null;
510
- }
511
- function createClientFromConnectionString(connectionString) {
512
- if (!connectionString) {
513
- throw new Error("flags: Missing connection string");
514
- }
515
- const connectionOptions = parseFlagsConnectionString(connectionString);
516
- const edgeConfigItemKey = connectionOptions.edgeConfigItemKey || "flags";
517
- if (!connectionOptions.edgeConfigId || !connectionOptions.edgeConfigToken) {
518
- throw new Error("flags: Missing edge config connection information");
519
- }
520
- const edgeConfigConnectionString = `https://edge-config.vercel.com/${connectionOptions.edgeConfigId}?token=${connectionOptions.edgeConfigToken}`;
521
- const edgeConfigClient = createEdgeConfigClient(edgeConfigConnectionString);
522
- const dataSource = new EdgeConfigDataSource({
523
- edgeConfigClient,
524
- edgeConfigItemKey,
525
- projectId: connectionOptions.projectId
526
- });
527
- const environment = getFlagsEnvironment(connectionOptions.env);
528
- return createClient({ dataSource, environment });
529
- }
530
- function getDefaultFlagsClient() {
531
- if (defaultFlagsClient)
532
- return defaultFlagsClient;
533
- if (!process.env.FLAGS) {
534
- throw new Error("flags: Missing environment variable FLAGS");
535
- }
536
- defaultFlagsClient = createClientFromConnectionString(process.env.FLAGS);
537
- return defaultFlagsClient;
538
- }
539
- function getFlagsEnvironment(connectionOptionsEnv) {
540
- if (connectionOptionsEnv) {
541
- return connectionOptionsEnv;
542
- }
543
- const vercelEnv = process.env.VERCEL_ENV;
544
- if (!vercelEnv || vercelEnv === "development") {
545
- return "development";
546
- }
547
- if (vercelEnv === "production") {
548
- return "production";
549
- }
550
- return "preview";
551
- }
552
-
553
- export {
554
- ResolutionReason,
555
- evaluate,
556
- createClient,
557
- store,
558
- EdgeConfigDataSource,
559
- InMemoryDataSource,
560
- parseFlagsConnectionString,
561
- resetDefaultFlagsClient,
562
- createClientFromConnectionString,
563
- getDefaultFlagsClient,
564
- getFlagsEnvironment
565
- };
566
- //# sourceMappingURL=chunk-4IFGPGNY.js.map