@avsbhq/core
v1.1.0
Published
Pure evaluation engine, shared types, and schemas for the A vs B SDK ecosystem.
Readme
@avsbhq/core
Pure evaluation engine, shared types, and schemas for the A vs B SDK ecosystem.
This package is the foundation that every other A vs B SDK package builds on. It contains no I/O, no DOM, no Node.js APIs — just the evaluator, type definitions, and shared helpers that all runtimes share.
Most application code should not import this package directly. Use @avsbhq/browser, @avsbhq/node, @avsbhq/react, or another runtime-specific package instead. Those packages re-export everything from @avsbhq/core you need at the application layer.
1. Install
npm install @avsbhq/coreCompatible with any JavaScript runtime: Node.js 18+, modern browsers, Cloudflare Workers, Deno, Bun, and React Native (Hermes).
No peer dependencies.
2. Quickstart
The primary use of @avsbhq/core in application code is importing its types. If you are building a per-language port of the A vs B SDK, use the /engine subpath for the evaluation primitives.
// Import types for type annotations in your own code
import type {
EvalContext,
Flag,
TrackPayload,
FlagDatafile,
} from '@avsbhq/core'
// Import the createFlag helper to construct Flag<T> objects in tests
import { createFlag, notFoundFlag } from '@avsbhq/core'
// Engine primitives — only for SDK implementors, not application code
import { evaluateFlag, murmurhash3 } from '@avsbhq/core/engine'For application-level flag evaluation, use the runtime SDKs:
// Browser / client-side:
import { AvsbClient } from '@avsbhq/browser'
// Server / Node.js:
import { AvsbServer } from '@avsbhq/node'3. SDK keys
Not applicable to @avsbhq/core. This package contains no networking code. SDK key handling lives in @avsbhq/browser (client keys) and @avsbhq/node (server keys).
4. Identity — EvalContext
EvalContext is the kinded context type used across every SDK. It replaces the legacy flat { attributes: Record<string, unknown> } bag.
export type EvalContext = SingleContext | MultiContext
// Single-kind context — the common case
interface SingleContext {
kind: string // 'user' is conventional; any string is valid
key: string // bucketing identifier (unique per kind)
name?: string // optional display name — never used in eval
_meta?: ContextMeta
[attribute: string]: unknown // targeting attributes
}
// Multi-kind context — user + organization + device simultaneously
interface MultiContext {
kind: 'multi'
[contextKind: string]: SingleContext | 'multi'
}Examples:
// Single context
const userCtx: EvalContext = {
kind: 'user',
key: 'u_123',
plan: 'pro',
country: 'US',
}
// Multi context
const multiCtx: EvalContext = {
kind: 'multi',
user: { kind: 'user', key: 'u_123', plan: 'pro' },
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
}Context helper functions exported from @avsbhq/core:
import {
isMultiContext,
getContextByKind,
userContext,
bucketingKey,
contextSummary,
privateAttributesFrom,
} from '@avsbhq/core'
isMultiContext(ctx) // type guard: ctx is MultiContext
getContextByKind(ctx, 'user') // extract a SingleContext from a multi-context
userContext('u_123', { plan: 'pro' }) // shorthand for {kind:'user',key:'u_123',plan:'pro'}
bucketingKey(ctx) // the key used for bucketing (kind-specific)
contextSummary(ctx) // [{kind,key}] — for decision logging (no attributes)5. Multi-context
@avsbhq/core defines the MultiContext type and the evaluator supports kinded targeting natively. A rule can target organization.tier while bucketing on user.key:
// Rule in the datafile:
// { hashAttribute: 'user.key', audienceIds: ['org-enterprise-audience'] }
//
// Audience condition:
// { contextKind: 'organization', attribute: 'tier', operator: 'equals', value: 'enterprise' }The resolveDottedAttribute helper (exported for conformance tests) resolves a dotted path like 'user.plan' against a multi-context:
import { resolveDottedAttribute } from '@avsbhq/core'
resolveDottedAttribute(multiCtx, 'user.plan') // → 'pro'
resolveDottedAttribute(multiCtx, 'organization.tier') // → 'enterprise'6. Reading flags — Flag<T> shape
Every evaluation in the SDK ecosystem returns a Flag<T>:
export interface Flag<T = unknown> {
readonly value: T // the variation value
readonly variationKey: string | null // null if served the default
readonly source: EvaluationSource
readonly ruleId: string | null
readonly ruleType: RuleType | null
readonly reasons: string[]
readonly evaluatedAt: number // ms epoch
readonly durationMicros: number
isEnabled(): boolean // source === 'rule' && value is truthy
exists(): boolean // source !== 'not_found'
}EvaluationSource values:
type EvaluationSource =
| 'datafileOverride' // override declared in dashboard
| 'runtimeOverride' // set via setOverrideForUser / setGlobalOverride
| 'sticky' // sticky bucketing assignment
| 'rule' // matched a targeting or rollout rule
| 'holdout' // user is in a holdout cohort
| 'bandit' // bandit algorithm decision
| 'default' // no rule matched; falling through to default
| 'not_found' // flag key does not exist in the datafile
| 'disabled' // flag is disabled globallyBuild Flag<T> objects in tests using the factory functions:
import { createFlag, notFoundFlag } from '@avsbhq/core'
const flag = createFlag<boolean>({
value: true,
variationKey: 'on',
source: 'rule',
ruleId: 'r_abc',
ruleType: 'ab_test',
reasons: ['matched audience: paid-users'],
})
const missing = notFoundFlag(false, 'flag key not in datafile')7. Tracking events
@avsbhq/core defines the TrackPayload interface. Event submission is handled by the runtime SDKs (@avsbhq/browser, @avsbhq/node).
export interface TrackPayload {
value?: number // numeric metric quantity (revenue, count, etc.)
properties?: Record<string, unknown> // free-form analytics properties
context?: EvalContext // server SDK: required unless via forUser()
}8. Error handling — Logger
@avsbhq/core exports the shared Logger interface and factory functions:
import {
createLogger,
noopLogger,
consoleLogger,
consoleTransport,
} from '@avsbhq/core'
// Logger interface:
interface Logger {
debug(message: string, data?: Record<string, unknown>): void
info(message: string, data?: Record<string, unknown>): void
warn(message: string, data?: Record<string, unknown>): void
error(message: string, data?: Record<string, unknown>): void
child(prefix: string): Logger
}
// Build a logger with multiple transports:
const logger = createLogger({
level: 'warn',
prefix: 'my-app',
transports: [consoleTransport({ level: 'warn' })],
})See @avsbhq/utils for pino and winston adapters.
9. SSR / hydration
Not applicable to @avsbhq/core. Hydration bootstrap logic lives in @avsbhq/browser (serializeBootstrap / hydrateBootstrap) and @avsbhq/next (AvsbHydrator, AvsbServerProvider).
10. Graceful shutdown
Not applicable to @avsbhq/core. Lifecycle management (polling, streaming, event flushing) lives in the runtime SDKs.
11. Testing — using @avsbhq/core in tests
When writing SDK port tests or testing code that handles Flag<T> objects, import the factory helpers directly:
import { createFlag, notFoundFlag, createEventBus } from '@avsbhq/core'
import { evaluateFlag } from '@avsbhq/core/engine'
// Build deterministic test flags
const FLAG_ENABLED = createFlag<boolean>({
value: true,
variationKey: 'on',
source: 'rule',
ruleId: 'r1',
ruleType: 'ab_test',
reasons: ['matched rule'],
})
// For application-level component and hook tests, use @avsbhq/test instead:
// import { createMockClient, AvsbTestProvider, TestData } from '@avsbhq/test'For application tests (components, hooks, route handlers), do not import @avsbhq/core/engine directly. Use @avsbhq/test mock utilities instead — they provide a stable, lifecycle-aware mock that does not depend on internal engine primitives.
12. Migration
There is no direct competitor to @avsbhq/core as a standalone pure-engine package. LaunchDarkly and Statsig embed their evaluation engines inside their runtime SDKs without a separately installable engine package. If you are migrating from either vendor and need to understand the shared types, the relevant comparison is:
| Concept | LaunchDarkly | Statsig | @avsbhq/core |
|---|---|---|---|
| Evaluation context | LDContext | StatsigUser | EvalContext (kinded) |
| Flag result | LDEvaluationDetail | DynamicConfig | Flag<T> |
| Event payload | LDCustomEventData | LogEvent | TrackPayload |
| Evaluation source | LDEvaluationReason | inferred | EvaluationSource |
Things to never call directly
Engine primitives (evaluateFlag, evaluateAudience, murmurhash3, bucketContext) live on the @avsbhq/core/engine subpath and are intentionally excluded from the default export.
Importing these directly in application code bypasses the tracker, the override system, sticky buckets, holdout logic, and the event bus. Only import from /engine when:
- You are implementing a per-language SDK port (Go, Python, Ruby, etc.)
- You are writing conformance tests against
@avsbhq/conformance - You are building a custom SDK layer
Do not import from @avsbhq/core/engine in application code or component tests. Use the runtime SDK (or @avsbhq/test for mocking) instead.
// Engine subpath — for SDK implementors only
import {
evaluateFlag,
evaluateAudience,
murmurhash3,
bucketContext,
evaluateBandit,
isHeldOut,
} from '@avsbhq/core/engine'