@beignet/core
Version:
Core framework primitives for Beignet
201 lines • 6.07 kB
JavaScript
/**
* @beignet/core/flags
*
* Provider-neutral feature flag primitives for Beignet applications.
*/
function createFlag(valueKind, key, options) {
const defaultValue = options.default;
if (!matchesValueKind(defaultValue, valueKind)) {
throw new Error(`Flag "${key}" default value does not match "${valueKind}" flag value kind.`);
}
return {
kind: "flag",
key,
valueKind: valueKind,
defaultValue,
description: options.description,
metadata: options.metadata,
};
}
function defineBooleanFlag(key, options) {
return createFlag("boolean", key, options);
}
function defineStringFlag(key, options) {
return createFlag("string", key, options);
}
function defineNumberFlag(key, options) {
return createFlag("number", key, options);
}
function defineObjectFlag(key, options) {
return createFlag("object", key, options);
}
/**
* Define one feature flag declaration.
*/
export const defineFlag = {
boolean: defineBooleanFlag,
string: defineStringFlag,
number: defineNumberFlag,
object: defineObjectFlag,
};
/**
* Define a typed registry of feature flags.
*/
export function defineFlags(registry) {
return registry;
}
/**
* Evaluate a flag through any `FlagsPort`.
*/
export function evaluateFlag(flags, flag, options) {
return flags.evaluate(flag, options);
}
/**
* Return detailed flag evaluation information through any `FlagsPort`.
*/
export function getFlagDetails(flags, flag, options) {
return flags.details(flag, options);
}
/**
* Create static flags backed by fixed values.
*/
export function createStaticFlags(options = {}) {
return createFlagsFromStore(() => options.values ?? {}, options);
}
/**
* Create mutable in-memory flags for tests, examples, and local development.
*/
export function createMemoryFlags(options = {}) {
const values = new Map(Object.entries(options.values ?? {}));
const port = createFlagsFromStore(() => Object.fromEntries(values), options);
return {
...port,
values,
set(flag, value) {
if (!matchesValueKind(value, flag.valueKind)) {
throw new Error(`Flag "${flag.key}" value does not match "${flag.valueKind}" flag value kind.`);
}
values.set(flag.key, value);
},
reset(flag) {
if (flag) {
values.delete(flag.key);
return;
}
values.clear();
},
};
}
function createFlagsFromStore(readValues, observers) {
async function resolve(flag, options, source) {
const startedAt = Date.now();
const defaultValue = options?.defaultValue ?? flag.defaultValue;
const values = readValues();
const stored = values[flag.key];
const hasStored = Object.hasOwn(values, flag.key);
const usedStoredValue = hasStored && matchesValueKind(stored, flag.valueKind);
const value = usedStoredValue
? stored
: defaultValue;
const details = {
key: flag.key,
valueKind: flag.valueKind,
value,
defaultValue,
reason: usedStoredValue ? "static" : "default",
defaulted: !usedStoredValue,
};
await observeEvaluation(observers.onEvaluation, {
source,
flag,
context: options?.context,
details,
durationMs: Date.now() - startedAt,
...correlationFromContext(options?.context),
});
if (options?.expose) {
await flags.recordExposure(flag, {
context: options.context,
value: details.value,
variant: details.variant,
metadata: details.metadata,
});
}
return details;
}
const flags = {
async evaluate(flag, options) {
const details = await resolve(flag, options, "evaluate");
return details.value;
},
async details(flag, options) {
return resolve(flag, options, "details");
},
async recordExposure(flag, options) {
await observeExposure(observers.onExposure, {
flag,
context: options?.context,
value: options?.value,
variant: options?.variant,
metadata: options?.metadata,
...correlationFromContext(options?.context),
});
},
async track(event, options) {
await observeTrack(observers.onTrack, {
event,
context: options?.context,
value: options?.value,
metadata: options?.metadata,
...correlationFromContext(options?.context),
});
},
};
return flags;
}
function matchesValueKind(value, kind) {
switch (kind) {
case "boolean":
return typeof value === "boolean";
case "string":
return typeof value === "string";
case "number":
return typeof value === "number" && Number.isFinite(value);
case "object":
return typeof value === "object" && value !== null;
}
}
function correlationFromContext(context) {
return {
requestId: context?.requestId,
traceId: context?.traceId,
spanId: context?.spanId,
parentSpanId: context?.parentSpanId,
traceparent: context?.traceparent,
};
}
async function observeEvaluation(observer, observation) {
try {
await observer?.(observation);
}
catch {
// Observers are diagnostic only.
}
}
async function observeExposure(observer, observation) {
try {
await observer?.(observation);
}
catch {
// Observers are diagnostic only.
}
}
async function observeTrack(observer, observation) {
try {
await observer?.(observation);
}
catch {
// Observers are diagnostic only.
}
}
//# sourceMappingURL=index.js.map