@variantlab/core 0.1.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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/_size/config.cjs +634 -0
- package/dist/_size/config.cjs.map +1 -0
- package/dist/_size/config.d.cts +2 -0
- package/dist/_size/config.d.ts +2 -0
- package/dist/_size/config.js +631 -0
- package/dist/_size/config.js.map +1 -0
- package/dist/_size/engine.cjs +1178 -0
- package/dist/_size/engine.cjs.map +1 -0
- package/dist/_size/engine.d.cts +2 -0
- package/dist/_size/engine.d.ts +2 -0
- package/dist/_size/engine.js +1175 -0
- package/dist/_size/engine.js.map +1 -0
- package/dist/_size/targeting.cjs +332 -0
- package/dist/_size/targeting.cjs.map +1 -0
- package/dist/_size/targeting.d.cts +97 -0
- package/dist/_size/targeting.d.ts +97 -0
- package/dist/_size/targeting.js +325 -0
- package/dist/_size/targeting.js.map +1 -0
- package/dist/config-B3DTOt1J.d.ts +46 -0
- package/dist/config-U0cqXPTa.d.cts +46 -0
- package/dist/engine-BEuGiH3G.d.cts +173 -0
- package/dist/engine-BoNBfZBL.d.ts +173 -0
- package/dist/index.cjs +1352 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +223 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +1324 -0
- package/dist/index.js.map +1 -0
- package/dist/types-BkXPpEyg.d.cts +82 -0
- package/dist/types-BkXPpEyg.d.ts +82 -0
- package/package.json +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/targeting/semver.ts","../../src/targeting/operators/app-version.ts","../../src/targeting/operators/attributes.ts","../../src/targeting/operators/locale.ts","../../src/targeting/operators/platform.ts","../../src/targeting/glob.ts","../../src/targeting/operators/routes.ts","../../src/targeting/operators/screen-size.ts","../../src/targeting/operators/user-id.ts","../../src/targeting/evaluator.ts","../../src/targeting/explain.ts","../../src/targeting/hash.ts"],"names":["v"],"mappings":"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","file":"targeting.js","sourcesContent":["/**\n * Hand-rolled semver subset. No dependencies, no regex on the hot path,\n * no backtracking. Supports the surface documented in\n * `docs/design/targeting-dsl.md`:\n *\n * - Comparators: `=`, `<`, `<=`, `>`, `>=`, and a bare version (treated as `=`)\n * - Caret: `^X.Y.Z` → `[>=X.Y.Z, <(X+1).0.0]`\n * - Tilde: `~X.Y.Z` → `[>=X.Y.Z, <X.(Y+1).0]`\n * - Hyphen range: `X.Y.Z - A.B.C` → `[>=X.Y.Z, <=A.B.C]`\n * - Compound (AND): space-separated comparators, e.g. `>=1.0.0 <2.0.0`\n * - OR: `||`-separated clauses\n *\n * Explicitly rejects prereleases (`1.0.0-beta`), build metadata\n * (`1.0.0+sha`), and `x` wildcards (`1.2.x`).\n */\n\nexport type Version = readonly [number, number, number];\nexport type Op = \"=\" | \"<\" | \"<=\" | \">\" | \">=\";\nexport interface Comparator {\n readonly op: Op;\n readonly v: Version;\n}\nexport type Clause = readonly Comparator[];\nexport type Range = readonly Clause[];\n\n/**\n * Pure character scanner. No regex. Returns `[major, minor, patch]`\n * or `null` if the string is not exactly three dot-separated\n * non-negative integers.\n */\nexport function parseVersion(s: string): Version | null {\n if (s.length === 0) return null;\n const parts: number[] = [0, 0, 0];\n let idx = 0;\n let part = 0;\n let seen = false;\n for (let i = 0; i < s.length; i++) {\n const c = s.charCodeAt(i);\n if (c === 46 /* . */) {\n if (!seen) return null;\n parts[idx++] = part;\n if (idx > 2) return null;\n part = 0;\n seen = false;\n } else if (c >= 48 && c <= 57) {\n part = part * 10 + (c - 48);\n seen = true;\n } else return null;\n }\n if (!seen || idx !== 2) return null;\n parts[2] = part;\n return [parts[0] as number, parts[1] as number, parts[2] as number];\n}\n\n/** Numeric comparison, field by field. */\nexport function cmpVersion(a: Version, b: Version): number {\n return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];\n}\n\n/**\n * Parse a semver range string. Returns a `Range` on success or `null`\n * on any syntactic or structural error.\n */\nexport function parseSemver(s: string): Range | null {\n if (s.length === 0) return null;\n const clauses: Clause[] = [];\n for (const part of s.split(\"||\")) {\n const c = parseClause(part.trim());\n if (c === null) return null;\n clauses.push(c);\n }\n return clauses;\n}\n\nfunction parseClause(s: string): Clause | null {\n if (s.length === 0) return null;\n\n // Hyphen range: `X.Y.Z - A.B.C`. Whitespace-sensitive separator.\n const hy = s.indexOf(\" - \");\n if (hy >= 0) {\n const lo = parseVersion(s.slice(0, hy).trim());\n const hi = parseVersion(s.slice(hy + 3).trim());\n if (lo === null || hi === null) return null;\n return [\n { op: \">=\", v: lo },\n { op: \"<=\", v: hi },\n ];\n }\n\n // Compound: whitespace-separated comparators, all ANDed.\n const cmps: Comparator[] = [];\n for (const tok of s.split(/\\s+/)) {\n if (tok.length === 0) continue;\n const parsed = parseComparator(tok);\n if (parsed === null) return null;\n for (const c of parsed) cmps.push(c);\n }\n return cmps.length > 0 ? cmps : null;\n}\n\n/**\n * Parse a single comparator like `>=1.2.3`, `^1.2.3`, `~1.2.3`, or a\n * bare version. Returns an array so that caret/tilde can expand to\n * two comparators.\n */\nfunction parseComparator(s: string): Comparator[] | null {\n const c0 = s[0];\n if (c0 === \"^\" || c0 === \"~\") {\n const v = parseVersion(s.slice(1));\n if (v === null) return null;\n const upper: Version = c0 === \"^\" ? [v[0] + 1, 0, 0] : [v[0], v[1] + 1, 0];\n return [\n { op: \">=\", v },\n { op: \"<\", v: upper },\n ];\n }\n\n let op: Op = \"=\";\n let rest = s;\n if (c0 === \">\" || c0 === \"<\") {\n if (s[1] === \"=\") {\n op = `${c0}=` as Op;\n rest = s.slice(2);\n } else {\n op = c0;\n rest = s.slice(1);\n }\n } else if (c0 === \"=\") {\n rest = s.slice(1);\n }\n\n const v = parseVersion(rest);\n return v === null ? null : [{ op, v }];\n}\n\n/** Match a parsed range against a parsed version. */\nexport function matchCompiled(range: Range, version: Version): boolean {\n for (const clause of range) if (matchClause(clause, version)) return true;\n return false;\n}\n\nfunction matchClause(clause: Clause, version: Version): boolean {\n for (const c of clause) {\n const d = cmpVersion(version, c.v);\n const op = c.op;\n const fail =\n op === \"=\"\n ? d !== 0\n : op === \"<\"\n ? d >= 0\n : op === \"<=\"\n ? d > 0\n : op === \">\"\n ? d <= 0\n : d < 0;\n if (fail) return false;\n }\n return true;\n}\n\n/** Convenience: parse both sides and compare. Returns false on parse error. */\nexport function matchSemver(range: string, version: string): boolean {\n const r = parseSemver(range);\n if (r === null) return false;\n const v = parseVersion(version);\n return v !== null && matchCompiled(r, v);\n}\n","/**\n * App version operator. Wraps the semver matcher. Fail-closed if the\n * context version is missing or unparseable.\n */\n\nimport { matchSemver } from \"../semver.js\";\n\nexport function matchAppVersion(range: string, ctxVersion: string | undefined): boolean {\n if (ctxVersion === undefined) return false;\n return matchSemver(range, ctxVersion);\n}\n","/**\n * Attributes operator. For each specified key, strict-equality\n * against `context.attributes[key]`. An empty targeting object\n * trivially matches (no constraints).\n */\n\nexport function matchAttributes(\n target: { readonly [key: string]: unknown },\n ctxAttrs: { readonly [key: string]: unknown } | undefined,\n): boolean {\n for (const k of Object.keys(target)) {\n if (ctxAttrs === undefined || ctxAttrs[k] !== target[k]) return false;\n }\n return true;\n}\n","/**\n * Locale operator. For each target in the array, matches if the\n * context locale equals the target OR starts with `target + \"-\"`.\n *\n * Targeting `\"en\"` matches `\"en\"`, `\"en-US\"`, `\"en-GB\"`.\n * Targeting `\"en-US\"` matches `\"en-US\"`, `\"en-US-POSIX\"` but not\n * `\"en\"` or `\"en-GB\"`.\n *\n * Case-sensitive — config authors are expected to normalize.\n */\n\nexport function matchLocale(target: ReadonlyArray<string>, ctxLocale: string | undefined): boolean {\n if (ctxLocale === undefined) return false;\n for (const t of target) {\n if (ctxLocale === t || ctxLocale.startsWith(`${t}-`)) return true;\n }\n return false;\n}\n","/**\n * Platform operator. Set membership; fails if the context's platform\n * is undefined. Linear scan is cheap — at most 4 elements per the\n * config allow-list.\n */\n\nexport function matchPlatform(\n target: ReadonlyArray<string>,\n ctxPlatform: string | undefined,\n): boolean {\n return ctxPlatform !== undefined && target.includes(ctxPlatform);\n}\n","/**\n * Hand-rolled route glob subset. Linear-time matcher, no regex, no\n * backtracking, no character classes. Supports the surface in\n * `docs/design/targeting-dsl.md`:\n *\n * - Exact: `/about`\n * - Wildcard segment: `/blog/*`\n * - Wildcard deep: `/docs/**` (only allowed as the last segment)\n * - Parameter: `/user/:id` (treated as a single-segment wildcard)\n * - Trailing slash insensitive\n * - Whole-path forms: `*` (single-segment) and `**` (any number of segments)\n *\n * Rejects: character classes, braces, negation, `***`, mixed\n * literal+wildcard in a single segment (e.g. `/foo*bar`).\n */\n\nexport type Segment =\n | { readonly kind: \"literal\"; readonly value: string }\n | { readonly kind: \"param\" }\n | { readonly kind: \"rest\" };\n\n/**\n * Compile a glob pattern into a segment list. Returns `null` for\n * unsupported / malformed patterns. Compiled patterns can be reused\n * and are the hot-path form.\n */\nexport function compileGlob(pattern: string): Segment[] | null {\n if (pattern.length === 0) return null;\n\n // Whole-path shortcuts. `*` and `**` have no leading slash.\n if (pattern === \"*\") return [{ kind: \"param\" }];\n if (pattern === \"**\") return [{ kind: \"rest\" }];\n\n if (pattern[0] !== \"/\") return null;\n\n // Reject `***` anywhere outright.\n if (pattern.indexOf(\"***\") >= 0) return null;\n\n // Strip a trailing slash (except for the root `/` itself).\n const normalized = pattern.length > 1 && pattern.endsWith(\"/\") ? pattern.slice(0, -1) : pattern;\n\n // Root: single empty segment list means \"match `/` exactly\".\n if (normalized === \"/\") return [];\n\n const raw = normalized.slice(1).split(\"/\");\n const segs: Segment[] = [];\n for (let i = 0; i < raw.length; i++) {\n const part = raw[i] as string;\n if (part.length === 0) return null; // e.g. `//` or trailing empty\n if (part === \"**\") {\n // `**` must be the last segment.\n if (i !== raw.length - 1) return null;\n segs.push({ kind: \"rest\" });\n continue;\n }\n if (part === \"*\") {\n segs.push({ kind: \"param\" });\n continue;\n }\n if (part[0] === \":\") {\n // `:id` — single-segment wildcard. Disallow empty name `:` alone.\n if (part.length < 2) return null;\n segs.push({ kind: \"param\" });\n continue;\n }\n // Literal segment. Reject any wildcard chars or colons inside.\n // Compile-time regex (not hot path); linear, no backtracking.\n if (/[*:?[\\]{}!]/.test(part)) return null;\n segs.push({ kind: \"literal\", value: part });\n }\n return segs;\n}\n\n/**\n * Match a compiled glob against a path. O(n + m) linear, no\n * backtracking because `**` is only ever the final segment.\n */\nexport function matchCompiledRoute(segs: readonly Segment[], path: string): boolean {\n if (path.length === 0) return false;\n if (path[0] !== \"/\") return false;\n\n // Strip a trailing slash (except for the root `/`).\n const normalized = path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n\n // Root `/` case: empty segment list matches exactly `/`.\n if (segs.length === 0) return normalized === \"/\";\n\n const parts = normalized === \"/\" ? [] : normalized.slice(1).split(\"/\");\n\n for (let i = 0; i < segs.length; i++) {\n const seg = segs[i] as Segment;\n if (seg.kind === \"rest\") {\n // `**` tail — matches zero or more remaining parts.\n return true;\n }\n if (i >= parts.length) return false;\n const part = parts[i] as string;\n if (part.length === 0) return false;\n if (seg.kind === \"literal\") {\n if (seg.value !== part) return false;\n }\n // `param` always matches a non-empty part.\n }\n return parts.length === segs.length;\n}\n\n/** Convenience: compile and match. Returns false on compile error. */\nexport function matchRoute(pattern: string, path: string): boolean {\n const segs = compileGlob(pattern);\n if (segs === null) return false;\n return matchCompiledRoute(segs, path);\n}\n","/**\n * Routes operator. Iterates patterns and returns true on the first\n * match. Each pattern is parsed per call in this session; Session 4's\n * engine will pre-compile at config load.\n */\n\nimport { matchRoute } from \"../glob.js\";\n\nexport function matchRoutes(target: ReadonlyArray<string>, ctxRoute: string | undefined): boolean {\n if (ctxRoute === undefined) return false;\n for (let i = 0; i < target.length; i++) {\n if (matchRoute(target[i] as string, ctxRoute)) return true;\n }\n return false;\n}\n","/**\n * Screen size operator. Set membership on pre-bucketed sizes. The\n * adapter (or the engine's context updater) is responsible for\n * deriving the bucket from actual pixel dimensions.\n */\n\nexport function matchScreenSize(\n target: ReadonlyArray<string>,\n ctxSize: string | undefined,\n): boolean {\n return ctxSize !== undefined && target.includes(ctxSize);\n}\n","/**\n * User ID operator. Two modes:\n *\n * 1. List: match if `context.userId` is in the explicit string list.\n * 2. Hash bucket: match if the precomputed `userIdBucket` (0..99)\n * is strictly less than `mod`. The evaluator is synchronous and\n * cannot compute sha256 inline; the engine precomputes this on\n * every `updateContext` call and stashes the result on the eval\n * context. If the bucket is missing, the operator fails closed.\n */\n\nimport type { EvalContext, Targeting } from \"../types.js\";\n\ntype Target = NonNullable<Targeting[\"userId\"]>;\n\nexport function matchUserId(target: Target, context: EvalContext): boolean {\n // `\"mod\" in target` narrows the discriminated union reliably — unlike\n // `Array.isArray()`, which does not narrow `ReadonlyArray<T>`.\n if (\"mod\" in target) {\n const bucket = context.userIdBucket;\n return typeof bucket === \"number\" && bucket < target.mod;\n }\n return context.userId !== undefined && target.includes(context.userId);\n}\n","/**\n * Synchronous targeting evaluator. Walks the operators in the order\n * documented in `docs/design/targeting-dsl.md` and short-circuits on\n * the first failure.\n *\n * Order: platform → screenSize → locale → appVersion → routes →\n * attributes → userId → predicate\n *\n * The `enabled` kill switch and `startDate`/`endDate` are experiment-\n * level and live in `explain()` + Session 4's engine — they're not\n * part of `evaluate()`.\n */\n\nimport { matchAppVersion } from \"./operators/app-version.js\";\nimport { matchAttributes } from \"./operators/attributes.js\";\nimport { matchLocale } from \"./operators/locale.js\";\nimport { matchPlatform } from \"./operators/platform.js\";\nimport { matchRoutes } from \"./operators/routes.js\";\nimport { matchScreenSize } from \"./operators/screen-size.js\";\nimport { matchUserId } from \"./operators/user-id.js\";\nimport type { EvalContext, EvaluableTargeting, TargetingResult, VariantContext } from \"./types.js\";\n\nexport function evaluate(\n targeting: EvaluableTargeting,\n context: VariantContext | EvalContext,\n): TargetingResult {\n const c = context as EvalContext;\n if (targeting.platform !== undefined && !matchPlatform(targeting.platform, c.platform))\n return { matched: false, reason: \"platform\" };\n if (targeting.screenSize !== undefined && !matchScreenSize(targeting.screenSize, c.screenSize))\n return { matched: false, reason: \"screenSize\" };\n if (targeting.locale !== undefined && !matchLocale(targeting.locale, c.locale))\n return { matched: false, reason: \"locale\" };\n if (targeting.appVersion !== undefined && !matchAppVersion(targeting.appVersion, c.appVersion))\n return { matched: false, reason: \"appVersion\" };\n if (targeting.routes !== undefined && !matchRoutes(targeting.routes, c.route))\n return { matched: false, reason: \"routes\" };\n if (targeting.attributes !== undefined && !matchAttributes(targeting.attributes, c.attributes))\n return { matched: false, reason: \"attributes\" };\n if (targeting.userId !== undefined && !matchUserId(targeting.userId, c))\n return { matched: false, reason: \"userId\" };\n if (targeting.predicate !== undefined && !targeting.predicate(c))\n return { matched: false, reason: \"predicate\" };\n return { matched: true };\n}\n\n/** Thin convenience wrapper — matches the `API.md` surface. */\nexport function matchTargeting(\n targeting: EvaluableTargeting,\n context: VariantContext | EvalContext,\n): boolean {\n return evaluate(targeting, context).matched;\n}\n","/**\n * Trace-producing evaluator. Walks startDate → endDate → targeting\n * (in the evaluator order), records every check performed, and\n * short-circuits at the first failure. The returned `steps` array\n * includes every check actually performed, ending with the failing\n * one when there is one.\n */\n\nimport { matchAppVersion } from \"./operators/app-version.js\";\nimport { matchAttributes } from \"./operators/attributes.js\";\nimport { matchLocale } from \"./operators/locale.js\";\nimport { matchPlatform } from \"./operators/platform.js\";\nimport { matchRoutes } from \"./operators/routes.js\";\nimport { matchScreenSize } from \"./operators/screen-size.js\";\nimport { matchUserId } from \"./operators/user-id.js\";\nimport type {\n EvalContext,\n EvaluableTargeting,\n Experiment,\n ExplainField,\n ExplainResult,\n ExplainStep,\n VariantContext,\n} from \"./types.js\";\n\nexport function explain(\n experiment: Experiment,\n context: VariantContext | EvalContext,\n): ExplainResult {\n const steps: ExplainStep[] = [];\n const record = (field: ExplainField, ok: boolean, detail: string): ExplainResult | null => {\n steps.push(ok ? { field, matched: true } : { field, matched: false, detail });\n return ok ? null : { matched: false, reason: field, steps };\n };\n\n if (experiment.startDate !== undefined) {\n const t = Date.parse(experiment.startDate);\n const r = record(\n \"startDate\",\n Number.isFinite(t) && Date.now() >= t,\n `now<${experiment.startDate}`,\n );\n if (r) return r;\n }\n if (experiment.endDate !== undefined) {\n const t = Date.parse(experiment.endDate);\n const r = record(\"endDate\", Number.isFinite(t) && Date.now() < t, `now>=${experiment.endDate}`);\n if (r) return r;\n }\n\n const tg = experiment.targeting as EvaluableTargeting | undefined;\n if (tg === undefined) return { matched: true, steps };\n const ctx = context as EvalContext;\n\n if (tg.platform !== undefined) {\n const r = record(\n \"platform\",\n matchPlatform(tg.platform, ctx.platform),\n `${ctx.platform} vs ${tg.platform}`,\n );\n if (r) return r;\n }\n if (tg.screenSize !== undefined) {\n const r = record(\n \"screenSize\",\n matchScreenSize(tg.screenSize, ctx.screenSize),\n `${ctx.screenSize} vs ${tg.screenSize}`,\n );\n if (r) return r;\n }\n if (tg.locale !== undefined) {\n const r = record(\"locale\", matchLocale(tg.locale, ctx.locale), `${ctx.locale} vs ${tg.locale}`);\n if (r) return r;\n }\n if (tg.appVersion !== undefined) {\n const r = record(\n \"appVersion\",\n matchAppVersion(tg.appVersion, ctx.appVersion),\n `${ctx.appVersion} vs ${tg.appVersion}`,\n );\n if (r) return r;\n }\n if (tg.routes !== undefined) {\n const r = record(\"routes\", matchRoutes(tg.routes, ctx.route), `${ctx.route} vs ${tg.routes}`);\n if (r) return r;\n }\n if (tg.attributes !== undefined) {\n const r = record(\"attributes\", matchAttributes(tg.attributes, ctx.attributes), \"mismatch\");\n if (r) return r;\n }\n if (tg.userId !== undefined) {\n const r = record(\"userId\", matchUserId(tg.userId, ctx), \"mismatch\");\n if (r) return r;\n }\n if (tg.predicate !== undefined) {\n const r = record(\"predicate\", tg.predicate(ctx), \"false\");\n if (r) return r;\n }\n return { matched: true, steps };\n}\n","/**\n * Web Crypto SHA-256 bucket hash for the `userId` hash-mod operator.\n * Returns an integer in [0, 99].\n *\n * The engine (Session 4) precomputes this on every `updateContext` and\n * stashes the result as `userIdBucket` in the eval context. The\n * operator reads it synchronously — `evaluate()` itself never awaits\n * anything.\n *\n * `globalThis.crypto.subtle` and `TextEncoder` are standardized in\n * Node 18.17+, browsers, Deno, Bun, and Edge runtimes. They are the\n * only platform globals `@variantlab/core` uses.\n */\n\ninterface CryptoLike {\n readonly subtle: {\n digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer>;\n };\n}\n\ndeclare class TextEncoder {\n encode(input?: string): Uint8Array;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function hashUserId(userId: string): Promise<number> {\n const bytes = encoder.encode(userId);\n const g = globalThis as unknown as { readonly crypto: CryptoLike };\n const buf = await g.crypto.subtle.digest(\"SHA-256\", bytes);\n const view = new DataView(buf);\n const uint32 = view.getUint32(0, false); // big-endian\n return uint32 % 100;\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { a as ExperimentsConfig } from './types-BkXPpEyg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Machine-readable issue codes for `ConfigValidationError`.
|
|
5
|
+
*
|
|
6
|
+
* This is a **type-only** module — no runtime export. Consumers
|
|
7
|
+
* that need to switch on the code do so against the string
|
|
8
|
+
* literals (the compiler narrows them).
|
|
9
|
+
*
|
|
10
|
+
* The list is append-only in minor versions.
|
|
11
|
+
*/
|
|
12
|
+
type IssueCode = "invalid-json" | "not-an-object" | "config-too-large" | "reserved-key" | "version/missing" | "version/invalid" | "enabled/invalid" | "signature/invalid" | "experiments/missing" | "experiments/not-an-array" | "experiments/too-many" | "experiment/not-an-object" | "experiment/missing-required" | "experiment/id/invalid" | "experiment/id/duplicate" | "experiment/name/invalid" | "experiment/description/invalid" | "experiment/type/invalid" | "experiment/status/invalid" | "experiment/variants/missing" | "experiment/variants/too-few" | "experiment/variants/too-many" | "variant/not-an-object" | "variant/id/invalid" | "variant/id/duplicate" | "variant/label/invalid" | "variant/description/invalid" | "experiment/default/missing" | "experiment/default/unknown-variant" | "experiment/assignment/invalid" | "split/missing" | "split/not-an-object" | "split/unknown-variant" | "split/value-invalid" | "split/sum-invalid" | "experiment/routes/invalid" | "route/glob/invalid" | "targeting/not-an-object" | "targeting/depth-exceeded" | "targeting/platform/invalid" | "targeting/appversion/invalid" | "targeting/locale/invalid" | "targeting/screensize/invalid" | "targeting/routes/invalid" | "targeting/userid/invalid" | "targeting/attributes/invalid" | "experiment/startdate/invalid" | "experiment/enddate/invalid" | "experiment/date-range/invalid" | "experiment/mutex/invalid" | "experiment/owner/invalid" | "experiment/overridable/invalid" | "rollback/not-an-object" | "rollback/threshold/invalid" | "rollback/window/invalid" | "rollback/persistent/invalid";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single config-validation issue.
|
|
16
|
+
*
|
|
17
|
+
* `path` is an RFC 6901 JSON Pointer into the source config
|
|
18
|
+
* (`""` for root, `"/experiments/0/variants/2/id"` for nested).
|
|
19
|
+
* `code` is a narrow union of strings for programmatic handling.
|
|
20
|
+
* `message` is a human-readable sentence safe for CLI output.
|
|
21
|
+
*/
|
|
22
|
+
interface ConfigIssue {
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly code: IssueCode;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by `validateConfig` when one or more rules fail.
|
|
29
|
+
*
|
|
30
|
+
* `issues` is always non-empty and frozen. The validator collects
|
|
31
|
+
* every issue it finds and throws once at the end — it does not
|
|
32
|
+
* fail fast — so a single throw can surface many independent
|
|
33
|
+
* problems in one pass.
|
|
34
|
+
*/
|
|
35
|
+
declare class ConfigValidationError extends Error {
|
|
36
|
+
readonly issues: readonly ConfigIssue[];
|
|
37
|
+
constructor(issues: readonly ConfigIssue[]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse, sanitize, validate, and deep-freeze an experiments config.
|
|
42
|
+
* Collects every issue before throwing (fail-slow, not fail-fast).
|
|
43
|
+
*/
|
|
44
|
+
declare function validateConfig(input: unknown): ExperimentsConfig;
|
|
45
|
+
|
|
46
|
+
export { type ConfigIssue as C, type IssueCode as I, ConfigValidationError as a, validateConfig as v };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { a as ExperimentsConfig } from './types-BkXPpEyg.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Machine-readable issue codes for `ConfigValidationError`.
|
|
5
|
+
*
|
|
6
|
+
* This is a **type-only** module — no runtime export. Consumers
|
|
7
|
+
* that need to switch on the code do so against the string
|
|
8
|
+
* literals (the compiler narrows them).
|
|
9
|
+
*
|
|
10
|
+
* The list is append-only in minor versions.
|
|
11
|
+
*/
|
|
12
|
+
type IssueCode = "invalid-json" | "not-an-object" | "config-too-large" | "reserved-key" | "version/missing" | "version/invalid" | "enabled/invalid" | "signature/invalid" | "experiments/missing" | "experiments/not-an-array" | "experiments/too-many" | "experiment/not-an-object" | "experiment/missing-required" | "experiment/id/invalid" | "experiment/id/duplicate" | "experiment/name/invalid" | "experiment/description/invalid" | "experiment/type/invalid" | "experiment/status/invalid" | "experiment/variants/missing" | "experiment/variants/too-few" | "experiment/variants/too-many" | "variant/not-an-object" | "variant/id/invalid" | "variant/id/duplicate" | "variant/label/invalid" | "variant/description/invalid" | "experiment/default/missing" | "experiment/default/unknown-variant" | "experiment/assignment/invalid" | "split/missing" | "split/not-an-object" | "split/unknown-variant" | "split/value-invalid" | "split/sum-invalid" | "experiment/routes/invalid" | "route/glob/invalid" | "targeting/not-an-object" | "targeting/depth-exceeded" | "targeting/platform/invalid" | "targeting/appversion/invalid" | "targeting/locale/invalid" | "targeting/screensize/invalid" | "targeting/routes/invalid" | "targeting/userid/invalid" | "targeting/attributes/invalid" | "experiment/startdate/invalid" | "experiment/enddate/invalid" | "experiment/date-range/invalid" | "experiment/mutex/invalid" | "experiment/owner/invalid" | "experiment/overridable/invalid" | "rollback/not-an-object" | "rollback/threshold/invalid" | "rollback/window/invalid" | "rollback/persistent/invalid";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single config-validation issue.
|
|
16
|
+
*
|
|
17
|
+
* `path` is an RFC 6901 JSON Pointer into the source config
|
|
18
|
+
* (`""` for root, `"/experiments/0/variants/2/id"` for nested).
|
|
19
|
+
* `code` is a narrow union of strings for programmatic handling.
|
|
20
|
+
* `message` is a human-readable sentence safe for CLI output.
|
|
21
|
+
*/
|
|
22
|
+
interface ConfigIssue {
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly code: IssueCode;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by `validateConfig` when one or more rules fail.
|
|
29
|
+
*
|
|
30
|
+
* `issues` is always non-empty and frozen. The validator collects
|
|
31
|
+
* every issue it finds and throws once at the end — it does not
|
|
32
|
+
* fail fast — so a single throw can surface many independent
|
|
33
|
+
* problems in one pass.
|
|
34
|
+
*/
|
|
35
|
+
declare class ConfigValidationError extends Error {
|
|
36
|
+
readonly issues: readonly ConfigIssue[];
|
|
37
|
+
constructor(issues: readonly ConfigIssue[]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse, sanitize, validate, and deep-freeze an experiments config.
|
|
42
|
+
* Collects every issue before throwing (fail-slow, not fail-fast).
|
|
43
|
+
*/
|
|
44
|
+
declare function validateConfig(input: unknown): ExperimentsConfig;
|
|
45
|
+
|
|
46
|
+
export { type ConfigIssue as C, type IssueCode as I, ConfigValidationError as a, validateConfig as v };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { a as ExperimentsConfig, b as VariantContext, E as Experiment } from './types-BkXPpEyg.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated union of engine events.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `API.md` §`EngineEvent`. Stored in the in-memory history
|
|
7
|
+
* ring buffer and emitted to `subscribe()` listeners. Time-travel
|
|
8
|
+
* replay (phase 4) reuses this exact shape.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type EngineEvent = {
|
|
12
|
+
readonly type: "ready";
|
|
13
|
+
readonly config: ExperimentsConfig;
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: "assignment";
|
|
16
|
+
readonly experimentId: string;
|
|
17
|
+
readonly variantId: string;
|
|
18
|
+
readonly context: VariantContext;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: "exposure";
|
|
21
|
+
readonly experimentId: string;
|
|
22
|
+
readonly variantId: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: "variantChanged";
|
|
25
|
+
readonly experimentId: string;
|
|
26
|
+
readonly variantId: string;
|
|
27
|
+
readonly source: "user" | "system" | "deeplink" | "qr";
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: "rollback";
|
|
30
|
+
readonly experimentId: string;
|
|
31
|
+
readonly variantId: string;
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: "configLoaded";
|
|
35
|
+
readonly config: ExperimentsConfig;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: "contextUpdated";
|
|
38
|
+
readonly context: VariantContext;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: "error";
|
|
41
|
+
readonly error: Error;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Listener set for engine events.
|
|
46
|
+
*
|
|
47
|
+
* Iteration copies the underlying Set so that a listener that
|
|
48
|
+
* unsubscribes itself (or another listener) during emission doesn't
|
|
49
|
+
* skip the next listener. Errors thrown inside a listener are
|
|
50
|
+
* swallowed — one bad subscriber must not break the engine for
|
|
51
|
+
* everyone else.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
type Listener = (event: EngineEvent) => void;
|
|
55
|
+
declare class ListenerSet {
|
|
56
|
+
private readonly listeners;
|
|
57
|
+
add(listener: Listener): () => void;
|
|
58
|
+
emit(event: EngineEvent): void;
|
|
59
|
+
clear(): void;
|
|
60
|
+
get size(): number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `VariantEngine`.
|
|
65
|
+
*
|
|
66
|
+
* Framework-agnostic runtime for `experiments.json`. Mirrors the
|
|
67
|
+
* surface in `API.md` §`VariantEngine` and preserves the
|
|
68
|
+
* non-negotiables from `ARCHITECTURE.md`:
|
|
69
|
+
*
|
|
70
|
+
* - `getVariant` is synchronous and O(1) after warmup.
|
|
71
|
+
* - No `Math.random` anywhere in the hot path.
|
|
72
|
+
* - `Date.now()` called once per resolution at the outer boundary.
|
|
73
|
+
* - Fail-open by default; fail-closed throws.
|
|
74
|
+
* - The loaded config is deeply frozen — no runtime mutation.
|
|
75
|
+
*
|
|
76
|
+
* Resolution order for `getVariant(id)`:
|
|
77
|
+
*
|
|
78
|
+
* 1. `dispose`d? → error path
|
|
79
|
+
* 2. in-memory override? → return override
|
|
80
|
+
* 3. cache hit? → return cached
|
|
81
|
+
* 4. experiment exists? → no → error path
|
|
82
|
+
* 5. rolled back? → return default
|
|
83
|
+
* 6. kill-switched? → return default
|
|
84
|
+
* 7. time-gated? → return default
|
|
85
|
+
* 8. targeting matches? → no → return default
|
|
86
|
+
* 9. mutex winner? → no → return default
|
|
87
|
+
* 10. assign → cache, emit, return
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
type FailMode = "fail-open" | "fail-closed";
|
|
91
|
+
type VariantChangeSource = "user" | "system" | "deeplink" | "qr";
|
|
92
|
+
interface EngineOptions {
|
|
93
|
+
readonly context?: VariantContext;
|
|
94
|
+
readonly failMode?: FailMode;
|
|
95
|
+
readonly historySize?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Pre-resolved assignments to seed the engine cache with at construction.
|
|
98
|
+
*
|
|
99
|
+
* Used by SSR adapters (e.g. `@variantlab/next`) to hydrate a client-side
|
|
100
|
+
* engine with the exact variants the server already rendered, so the first
|
|
101
|
+
* `getVariant` call short-circuits on a cache hit instead of re-evaluating
|
|
102
|
+
* targeting/assignment and risking hydration mismatches.
|
|
103
|
+
*
|
|
104
|
+
* Entries whose experiment id is not in the config, or whose variant id is
|
|
105
|
+
* not in the experiment's variants, are silently ignored. Seeding does NOT
|
|
106
|
+
* emit an `assignment` event — events fire on the first real `getVariant`
|
|
107
|
+
* call. `updateContext`/`loadConfig` still clear the cache.
|
|
108
|
+
*/
|
|
109
|
+
readonly initialAssignments?: Readonly<Record<string, string>>;
|
|
110
|
+
}
|
|
111
|
+
/** Thrown in fail-closed mode when an experiment id isn't in the config. */
|
|
112
|
+
declare class UnknownExperimentError extends Error {
|
|
113
|
+
readonly experimentId: string;
|
|
114
|
+
constructor(experimentId: string);
|
|
115
|
+
}
|
|
116
|
+
declare class VariantEngine {
|
|
117
|
+
private config;
|
|
118
|
+
private experimentIndex;
|
|
119
|
+
private context;
|
|
120
|
+
private evalContext;
|
|
121
|
+
private readonly failMode;
|
|
122
|
+
private readonly history;
|
|
123
|
+
private readonly listeners;
|
|
124
|
+
private readonly overrides;
|
|
125
|
+
private readonly cache;
|
|
126
|
+
private readonly rolledBack;
|
|
127
|
+
private readonly crashCounter;
|
|
128
|
+
private disposed;
|
|
129
|
+
constructor(config: ExperimentsConfig, options?: EngineOptions);
|
|
130
|
+
/**
|
|
131
|
+
* Copy caller-supplied `{ experimentId: variantId }` pairs into the
|
|
132
|
+
* in-memory cache so the next `getVariant` call returns them without
|
|
133
|
+
* re-evaluating targeting. Invalid pairs are silently dropped — seeding
|
|
134
|
+
* is best-effort hydration, never a hard error.
|
|
135
|
+
*/
|
|
136
|
+
private seedInitialAssignments;
|
|
137
|
+
getVariant(experimentId: string): string;
|
|
138
|
+
getVariantValue<T = unknown>(experimentId: string): T;
|
|
139
|
+
setVariant(experimentId: string, variantId: string, source?: VariantChangeSource): void;
|
|
140
|
+
clearVariant(experimentId: string): void;
|
|
141
|
+
resetAll(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Returns the currently loaded, deeply-frozen {@link ExperimentsConfig}.
|
|
144
|
+
*
|
|
145
|
+
* Exposed read-only so debug surfaces (e.g. `<VariantDebugOverlay />`)
|
|
146
|
+
* can display the raw config without reaching into private fields.
|
|
147
|
+
* Mutating this value has no effect on the engine.
|
|
148
|
+
*/
|
|
149
|
+
getConfig(): ExperimentsConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Returns a shallow copy of the current {@link VariantContext}.
|
|
152
|
+
*
|
|
153
|
+
* A copy is returned rather than the live object so callers can't
|
|
154
|
+
* accidentally mutate engine state. Use {@link updateContext} to
|
|
155
|
+
* change it.
|
|
156
|
+
*/
|
|
157
|
+
getContext(): VariantContext;
|
|
158
|
+
getExperiments(route?: string): readonly Experiment[];
|
|
159
|
+
subscribe(listener: Listener): () => void;
|
|
160
|
+
updateContext(patch: Partial<VariantContext>): void;
|
|
161
|
+
loadConfig(next: unknown): Promise<void>;
|
|
162
|
+
reportCrash(experimentId: string, _error: Error): void;
|
|
163
|
+
getHistory(): readonly EngineEvent[];
|
|
164
|
+
dispose(): void;
|
|
165
|
+
private isTargeted;
|
|
166
|
+
private resolveMutexWinner;
|
|
167
|
+
private emit;
|
|
168
|
+
private handleError;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
declare function createEngine(config: unknown, options?: EngineOptions): VariantEngine;
|
|
172
|
+
|
|
173
|
+
export { type EngineEvent as E, type FailMode as F, type Listener as L, UnknownExperimentError as U, type VariantChangeSource as V, type EngineOptions as a, ListenerSet as b, VariantEngine as c, createEngine as d };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { a as ExperimentsConfig, b as VariantContext, E as Experiment } from './types-BkXPpEyg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated union of engine events.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `API.md` §`EngineEvent`. Stored in the in-memory history
|
|
7
|
+
* ring buffer and emitted to `subscribe()` listeners. Time-travel
|
|
8
|
+
* replay (phase 4) reuses this exact shape.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type EngineEvent = {
|
|
12
|
+
readonly type: "ready";
|
|
13
|
+
readonly config: ExperimentsConfig;
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: "assignment";
|
|
16
|
+
readonly experimentId: string;
|
|
17
|
+
readonly variantId: string;
|
|
18
|
+
readonly context: VariantContext;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: "exposure";
|
|
21
|
+
readonly experimentId: string;
|
|
22
|
+
readonly variantId: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: "variantChanged";
|
|
25
|
+
readonly experimentId: string;
|
|
26
|
+
readonly variantId: string;
|
|
27
|
+
readonly source: "user" | "system" | "deeplink" | "qr";
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: "rollback";
|
|
30
|
+
readonly experimentId: string;
|
|
31
|
+
readonly variantId: string;
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: "configLoaded";
|
|
35
|
+
readonly config: ExperimentsConfig;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: "contextUpdated";
|
|
38
|
+
readonly context: VariantContext;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: "error";
|
|
41
|
+
readonly error: Error;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Listener set for engine events.
|
|
46
|
+
*
|
|
47
|
+
* Iteration copies the underlying Set so that a listener that
|
|
48
|
+
* unsubscribes itself (or another listener) during emission doesn't
|
|
49
|
+
* skip the next listener. Errors thrown inside a listener are
|
|
50
|
+
* swallowed — one bad subscriber must not break the engine for
|
|
51
|
+
* everyone else.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
type Listener = (event: EngineEvent) => void;
|
|
55
|
+
declare class ListenerSet {
|
|
56
|
+
private readonly listeners;
|
|
57
|
+
add(listener: Listener): () => void;
|
|
58
|
+
emit(event: EngineEvent): void;
|
|
59
|
+
clear(): void;
|
|
60
|
+
get size(): number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `VariantEngine`.
|
|
65
|
+
*
|
|
66
|
+
* Framework-agnostic runtime for `experiments.json`. Mirrors the
|
|
67
|
+
* surface in `API.md` §`VariantEngine` and preserves the
|
|
68
|
+
* non-negotiables from `ARCHITECTURE.md`:
|
|
69
|
+
*
|
|
70
|
+
* - `getVariant` is synchronous and O(1) after warmup.
|
|
71
|
+
* - No `Math.random` anywhere in the hot path.
|
|
72
|
+
* - `Date.now()` called once per resolution at the outer boundary.
|
|
73
|
+
* - Fail-open by default; fail-closed throws.
|
|
74
|
+
* - The loaded config is deeply frozen — no runtime mutation.
|
|
75
|
+
*
|
|
76
|
+
* Resolution order for `getVariant(id)`:
|
|
77
|
+
*
|
|
78
|
+
* 1. `dispose`d? → error path
|
|
79
|
+
* 2. in-memory override? → return override
|
|
80
|
+
* 3. cache hit? → return cached
|
|
81
|
+
* 4. experiment exists? → no → error path
|
|
82
|
+
* 5. rolled back? → return default
|
|
83
|
+
* 6. kill-switched? → return default
|
|
84
|
+
* 7. time-gated? → return default
|
|
85
|
+
* 8. targeting matches? → no → return default
|
|
86
|
+
* 9. mutex winner? → no → return default
|
|
87
|
+
* 10. assign → cache, emit, return
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
type FailMode = "fail-open" | "fail-closed";
|
|
91
|
+
type VariantChangeSource = "user" | "system" | "deeplink" | "qr";
|
|
92
|
+
interface EngineOptions {
|
|
93
|
+
readonly context?: VariantContext;
|
|
94
|
+
readonly failMode?: FailMode;
|
|
95
|
+
readonly historySize?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Pre-resolved assignments to seed the engine cache with at construction.
|
|
98
|
+
*
|
|
99
|
+
* Used by SSR adapters (e.g. `@variantlab/next`) to hydrate a client-side
|
|
100
|
+
* engine with the exact variants the server already rendered, so the first
|
|
101
|
+
* `getVariant` call short-circuits on a cache hit instead of re-evaluating
|
|
102
|
+
* targeting/assignment and risking hydration mismatches.
|
|
103
|
+
*
|
|
104
|
+
* Entries whose experiment id is not in the config, or whose variant id is
|
|
105
|
+
* not in the experiment's variants, are silently ignored. Seeding does NOT
|
|
106
|
+
* emit an `assignment` event — events fire on the first real `getVariant`
|
|
107
|
+
* call. `updateContext`/`loadConfig` still clear the cache.
|
|
108
|
+
*/
|
|
109
|
+
readonly initialAssignments?: Readonly<Record<string, string>>;
|
|
110
|
+
}
|
|
111
|
+
/** Thrown in fail-closed mode when an experiment id isn't in the config. */
|
|
112
|
+
declare class UnknownExperimentError extends Error {
|
|
113
|
+
readonly experimentId: string;
|
|
114
|
+
constructor(experimentId: string);
|
|
115
|
+
}
|
|
116
|
+
declare class VariantEngine {
|
|
117
|
+
private config;
|
|
118
|
+
private experimentIndex;
|
|
119
|
+
private context;
|
|
120
|
+
private evalContext;
|
|
121
|
+
private readonly failMode;
|
|
122
|
+
private readonly history;
|
|
123
|
+
private readonly listeners;
|
|
124
|
+
private readonly overrides;
|
|
125
|
+
private readonly cache;
|
|
126
|
+
private readonly rolledBack;
|
|
127
|
+
private readonly crashCounter;
|
|
128
|
+
private disposed;
|
|
129
|
+
constructor(config: ExperimentsConfig, options?: EngineOptions);
|
|
130
|
+
/**
|
|
131
|
+
* Copy caller-supplied `{ experimentId: variantId }` pairs into the
|
|
132
|
+
* in-memory cache so the next `getVariant` call returns them without
|
|
133
|
+
* re-evaluating targeting. Invalid pairs are silently dropped — seeding
|
|
134
|
+
* is best-effort hydration, never a hard error.
|
|
135
|
+
*/
|
|
136
|
+
private seedInitialAssignments;
|
|
137
|
+
getVariant(experimentId: string): string;
|
|
138
|
+
getVariantValue<T = unknown>(experimentId: string): T;
|
|
139
|
+
setVariant(experimentId: string, variantId: string, source?: VariantChangeSource): void;
|
|
140
|
+
clearVariant(experimentId: string): void;
|
|
141
|
+
resetAll(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Returns the currently loaded, deeply-frozen {@link ExperimentsConfig}.
|
|
144
|
+
*
|
|
145
|
+
* Exposed read-only so debug surfaces (e.g. `<VariantDebugOverlay />`)
|
|
146
|
+
* can display the raw config without reaching into private fields.
|
|
147
|
+
* Mutating this value has no effect on the engine.
|
|
148
|
+
*/
|
|
149
|
+
getConfig(): ExperimentsConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Returns a shallow copy of the current {@link VariantContext}.
|
|
152
|
+
*
|
|
153
|
+
* A copy is returned rather than the live object so callers can't
|
|
154
|
+
* accidentally mutate engine state. Use {@link updateContext} to
|
|
155
|
+
* change it.
|
|
156
|
+
*/
|
|
157
|
+
getContext(): VariantContext;
|
|
158
|
+
getExperiments(route?: string): readonly Experiment[];
|
|
159
|
+
subscribe(listener: Listener): () => void;
|
|
160
|
+
updateContext(patch: Partial<VariantContext>): void;
|
|
161
|
+
loadConfig(next: unknown): Promise<void>;
|
|
162
|
+
reportCrash(experimentId: string, _error: Error): void;
|
|
163
|
+
getHistory(): readonly EngineEvent[];
|
|
164
|
+
dispose(): void;
|
|
165
|
+
private isTargeted;
|
|
166
|
+
private resolveMutexWinner;
|
|
167
|
+
private emit;
|
|
168
|
+
private handleError;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
declare function createEngine(config: unknown, options?: EngineOptions): VariantEngine;
|
|
172
|
+
|
|
173
|
+
export { type EngineEvent as E, type FailMode as F, type Listener as L, UnknownExperimentError as U, type VariantChangeSource as V, type EngineOptions as a, ListenerSet as b, VariantEngine as c, createEngine as d };
|