Package

featureflags

Feature flags: targeting rules, segments, prerequisites, stable percentage rollouts, OpenFeature-style details, JSON definitions and a SQLite store with audit

0.1.0 MIT db fs

Install

sspur add featureflags@^0.1.0

or in sspur.toml:

featureflags = { version = "^0.1.0" }

README

featureflags

Feature flags for SSPUR services: boolean and multivariate flags (string, integer and number variants), explicit allow and deny lists, targeting rules over context attributes, reusable segments, prerequisites, and stable percentage rollouts and weighted splits. Evaluation is a pure function of a snapshot and a context and returns OpenFeature-style details: the value, the variant, the reason (STATIC, DEFAULT, TARGETING_MATCH, SPLIT, DISABLED, PREREQUISITE_FAILED, ERROR) and an error code (FLAG_NOT_FOUND, TYPE_MISMATCH, TARGETING_KEY_MISSING, GENERAL, ...). Definitions are a JSON format that is validated with errors that name the exact path, and they can be loaded from a file or kept in SQLite with versioned, optimistic-concurrency updates and an audit history.

Install

$ sspur add featureflags

Example

flags.json:

{"flags": [
  {"key": "new-checkout",
   "variants": [{"name": "on", "boolean": true}, {"name": "off", "boolean": false}],
   "off_variant": "off",
   "targets": [{"variant": "off", "keys": ["user-13"]}],
   "rules": [{"id": "staff", "when": {"segment": "staff"}, "serve": {"variant": "on"}}],
   "fallthrough": {"split": [{"variant": "on", "weight": 25}, {"variant": "off", "weight": 75}]}},
  {"key": "theme",
   "variants": [{"name": "light", "string": "light"}, {"name": "dark", "string": "dark"}],
   "off_variant": "light",
   "rules": [{"id": "night-owls", "when": {"attr": "hour", "op": "ge", "values": ["20"]}, "serve": {"variant": "dark"}}],
   "fallthrough": {"variant": "light"}}
 ],
 "segments": [{"key": "staff", "rules": [{"attr": "email", "op": "ends_with", "values": ["@example.com"]}]}]}

main.ssp:

use featureflags.{context, with_attr, evaluate, is_on, str_value, reason_name}

fn main() -> Unit ! fs, db, log, time, fail[DbErr], fail[featureflags.StoreErr]
= match featureflags.load_file("flags.json")
  | err(e) => log("bad flags: {e}")
  | ok(s) => do
    ada = context("user-1").with_attr("email", "ada@example.com")
    d = evaluate(s, "new-checkout", ada)
    log("ada: {d.value}, {reason_name(d.reason)}, rule {d.rule}")
    log("user-13: {is_on(s, "new-checkout", context("user-13"), true)}")
    on = (0..10000).filter(i => is_on(s, "new-checkout", context("user-{i}"), false)).len
    log("rollout: {on} of 10000 users")
    log("theme at 21h: {str_value(s, "theme", context("u").with_attr("hour", "21"), "light")}")
    log("missing flag: {reason_name(evaluate(s, "nope", ada).reason)}")
    c = db.open(":memory:")
    featureflags.open_store(c)
    v = featureflags.import_snapshot(c, s, "deploy", now())
    f = featureflags.put_flag(c, featureflags.bool_flag("kill-switch", false), some(0), "ada", now())
    log("store version {featureflags.store_version(c)}, kill-switch v{f.version}")
    log("history: {featureflags.history(c, "flag", "kill-switch").map(e => "{e.action} by {e.actor}")}")
$ sspur run main.ssp
ada: some(BoolV{v: true}), TARGETING_MATCH, rule some("staff")
user-13: false
rollout: 2521 of 10000 users
theme at 21h: dark
missing flag: ERROR
store version 4, kill-switch v1
history: ["put by ada"]

How a flag is evaluated

  1. A disabled flag serves its off_variant (DISABLED).
  2. Each prerequisite flag must evaluate to one of its listed variants, or the flag serves off_variant (PREREQUISITE_FAILED). Prerequisite cycles are a validation error, and an ERROR if a snapshot with one is evaluated anyway.
  3. Explicit targets: a context whose targeting key is listed gets that variant (TARGETING_MATCH, rule target:VARIANT). Use them as allow and deny lists.
  4. Rules in order: the first whose condition holds serves its variant (TARGETING_MATCH) or its split (SPLIT).
  5. Otherwise the fallthrough: STATIC when the flag has no targets or rules, else DEFAULT, or SPLIT for a split.

Typed accessors (is_on, str_value, int_value, num_value and their *_details forms) return the caller's default on any error, with the error in the details, so a missing or mistyped flag never breaks a request.

Bucketing

A split hashes flag key + "." + salt + "." + bucketing key with SHA-256 and maps the first four bytes to a bucket from 0 to 99999; the weights (any non-negative integers, such as percentages or parts of 100000) are laid out in order over the buckets. So a context always gets the same variant in every process and in both tiers, buckets are uniform (the tests check 20000 keys in ten bands, and a 10% and 30% rollout of 5000 keys), and raising a rollout's percentage keeps everyone who already had the new variant. Changing the flag's salt reshuffles it; bucket_by buckets by another attribute (an organization id, say), and a context without that attribute gets TARGETING_KEY_MISSING.

Conditions

AttrClause{attr, op, values} compares a context attribute (key is the targeting key unless the attributes define key). Operators: equals, not_equals (one value), in, not_in, contains, not_contains, starts_with, ends_with, matches (any of the values; matches takes regular expressions, which the stdlib runs in linear time, so a pattern can't hang a server), lt, le, gt, ge (numbers), before, after (ISO 8601 times), exists, not_exists. A missing attribute fails every operator except not_exists, negated ones included. Combine with all, any and negate; {"segment": "staff"} tests segment membership (excluded keys never, included keys always, else any of its rules).

API

Contexts and values

  • Context{key, attrs}, context(key), with_attr(c, name, value), attr(c, name).
  • FlagValue (BoolV, StrV, IntV, NumV), type_name(v).

Definitions

  • Flag, Segment, Rule, Target, Prereq, Serve (Fixed{variant}, Weighted{weights, bucket_by}), Clause (AttrClause, SegmentClause, AllClauses, AnyClause, NotClause, AlwaysClause), Op.
  • Builders: bool_flag(key, enabled), string_flag(key, enabled, variants, default, off), add_rule, add_target, add_prereq, with_fallthrough, rule(id, attr, op, values, serve), rollout(on, off, pct).
  • Snapshot{flags, segments, version}, snapshot(flags, segments): an immutable value that request handlers can share.
  • problems(s): every validation problem with its path; empty when valid.

Evaluation

  • evaluate(s, key, ctx) gives Details{key, value, variant, reason, error, message, rule, flag_version}. evaluate_all(s, ctx): every flag, for bootstrapping a client.
  • is_on, str_value, int_value, num_value; bool_details, str_details, int_details, num_details give Resolved[T]{value, details}.
  • Reason, reason_name, ErrorCode, error_name (OpenFeature spellings).
  • clause_holds(s, c, ctx), in_segment(s, seg, ctx), bucket(flag, salt, key), split_variant(weights, bucket).

JSON format

  • decode(text) (validated snapshot, or an error such as flags[1].rules[0].when.op: unknown operator 'eqals'), encode(s), load_file(path), fingerprint(s) (SHA-256 of the definitions, to tell whether anything changed).
  • decode_flag, encode_flag, decode_segment, encode_segment: one definition.
  • Wire types: DefinitionsJson, FlagJson, VariantJson (a name and exactly one of boolean, string, integer, number), ClauseJson (exactly one of attr/op/values, segment, all, any, negate, always), ServeJson (variant, or split with optional bucket_by), WeightJson, RuleJson, SegmentJson. op_name, parse_op.

SQLite store

  • open_store(c): create the tables (featureflags_flags, featureflags_segments, featureflags_audit, featureflags_meta).
  • put_flag(c, f, expected, actor, at), delete_flag, put_segment, delete_segment: each a transaction that keeps the whole store valid (a flag can't name a missing segment, and a flag or segment still in use can't be deleted), with optimistic concurrency (expected is the version the caller read; a concurrent change is Conflict) and an audit entry.
  • import_snapshot(c, s, actor, at): replace everything, auditing each change.
  • load_store(c), store_version(c) (one row; compare with Snapshot.version), reload_if_changed(c, have).
  • evaluate_stored(c, key, ctx): evaluate from the store, reading only the flag, its prerequisites and their segments; cheap enough for every request of a handler that shares the Db.
  • history(c, kind, key), changes_since(c, version): AuditEntry{id, kind, key, action, definition, version, actor, at}.
  • StoreErr: Invalid{msg}, Conflict{key, expected, actual}, NotFound{key}; raised from an http.serve handler they answer 400, 409 and 404.

Out of scope

JSON-object variants (store JSON text in a string variant), semantic-version operators, streaming updates to remote clients, experiment analytics and exposure events (log Details yourself), and a hosted admin UI. Handlers can't keep a mutable cache between requests, so a service either shares one immutable Snapshot (and restarts, or polls reload_if_changed from a loop outside the handlers) or calls evaluate_stored per request.

License

MIT

API

FlagValue type

pub type FlagValue = BoolV{v: Bool} | StrV{v: Str} | IntV{v: Int} | NumV{v: F64} derive Eq, Show

The value of one variant.

type_name fn

fn type_name(v: FlagValue) -> Str

The name of a value's type: boolean, string, integer or number.

Context type

pub type Context = {key: Str, attrs: Map[Str, Str]} derive Eq, Show

Who a flag is evaluated for: the targeting key (a user, account or device id; used for explicit targets and as the default bucketing key) and string attributes. A rule's attribute key means the targeting key unless the attributes define key themselves.

context fn

fn context(key: Str) -> Context

A context for key with no attributes.

with_attr fn

fn with_attr(c: Context, name: Str, value: Str) -> Context

The context with attribute name set to value.

attr fn

fn attr(c: Context, name: Str) -> Opt[Str]

The value of attribute name in c, or none. key falls back to the targeting key, and an empty targeting key counts as missing.

Op type

pub type Op = Equals | NotEquals | In | NotIn | Contains | NotContains | StartsWith | EndsWith | Lt | Le | Gt | Ge | Matches | Before | After | Exists | NotExists derive Eq, Show

A comparison in a rule. Every operator except NotExists is false when the attribute is missing. Equals and NotEquals take one value, In and NotIn one or more, the string operators (Contains, NotContains, StartsWith, EndsWith) and Matches (a regular expression; the stdlib engine runs in linear time, so a pattern can't hang the server) are true when any value fits, Lt Le Gt Ge compare numbers (false if either side isn't one), Before and After compare ISO 8601 times, and Exists and NotExists take no values. Comparisons are exact and case-sensitive.

Clause type

pub type Clause = AttrClause{attr: Str, op: Op, values: List[Str]} | SegmentClause{segment: Str} | AllClauses{cs: List[Clause]} | AnyClause{cs: List[Clause]} | NotClause{c: Clause} | AlwaysClause derive Eq, Show

A targeting condition: an attribute comparison, membership of a segment, or a combination. AllClauses([]) is true and AnyClause([]) is false.

Serve type

pub type Serve = Fixed{variant: Str} | Weighted{weights: List[(Str, Int)], bucket_by: Str} derive Eq, Show

What a rule (or the fallthrough) serves: one variant, or a weighted split of variants by a stable hash of the bucketing attribute (bucket_by, key for the targeting key). Weights are relative non-negative integers (percentages, or parts of 100000 for finer steps).

Rule type

pub type Rule = {id: Str, when: Clause, serve: Serve} derive Eq, Show

A targeting rule: when when holds, serve serve. id names it in evaluation details.

Target type

pub type Target = {variant: Str, keys: List[Str]} derive Eq, Show

Contexts whose targeting key is in keys get variant, before any rule: allow and deny lists.

Prereq type

pub type Prereq = {flag: Str, variants: List[Str]} derive Eq, Show

The flag flag must evaluate to one of variants for this flag to be on.

Flag type

pub type Flag = {key: Str, enabled: Bool, variants: List[(Str, FlagValue)], off_variant: Str, fallthrough: Serve, targets: List[Target], rules: List[Rule], prerequisites: List[Prereq], salt: Str, version: Int} derive Eq, Show

A flag. When enabled is false it serves off_variant. Otherwise its prerequisites must hold (else off_variant), then explicit targets, then the first matching rule, then fallthrough decide. salt changes every bucket of the flag at once (leave it "" to bucket by the key alone); version counts changes.

Segment type

pub type Segment = {key: Str, included: List[Str], excluded: List[Str], rules: List[Clause]} derive Eq, Show

A reusable set of contexts: excluded keys never belong, included keys always do, and otherwise a context belongs when any of rules holds. Segment rules may not refer to segments.

Snapshot type

pub type Snapshot = {flags: Map[Str, Flag], segments: Map[Str, Segment], version: Int} derive Eq, Show

A set of flags and segments, with the version of the store they came from (0 for a file). Share it between request handlers as any immutable value.

bool_flag fn

fn bool_flag(key: Str, enabled: Bool) -> Flag

A boolean flag: on (true) and off (false), off when disabled, on for everyone when enabled unless rules say otherwise.

string_flag fn

fn string_flag(key: Str, enabled: Bool, variants: List[(Str, Str)], default_variant: Str, off_variant: Str) -> Flag

A flag with string variants, serving default_variant to everyone when enabled and off_variant when disabled.

add_rule fn

fn add_rule(f: Flag, rule: Rule) -> Flag

The flag with rule added after its other rules.

add_target fn

fn add_target(f: Flag, variant: Str, keys: List[Str]) -> Flag

The flag with an explicit target: keys get variant.

add_prereq fn

fn add_prereq(f: Flag, flag: Str, variants: List[Str]) -> Flag

The flag with a prerequisite.

with_fallthrough fn

fn with_fallthrough(f: Flag, s: Serve) -> Flag

The flag serving s when no target or rule decides.

rollout fn

fn rollout(on: Str, off: Str, pct: F64 where _ >= 0.0 and _ <= 100.0) -> Serve

A percentage rollout: pct percent (0 to 100, in steps of 0.001) of contexts, by targeting key, get on, the rest off. Raising the percentage later keeps everyone who already had on.

rule fn

fn rule(id: Str, attr: Str, op: Op, values: List[Str], serve: Serve) -> Rule

A rule matching one attribute.

snapshot fn

fn snapshot(flags: List[Flag], segments: List[Segment]) -> Snapshot

A snapshot of these flags and segments (later ones replace earlier ones with the same key).

bucket fn

fn bucket(flag_key: Str, salt: Str, bucket_key: Str) -> Int

The bucket of bucket_key for a flag, from 0 to 99999: the first four bytes of SHA-256(flag_key "." salt "." bucket_key), as an unsigned big-endian integer, scaled to

  1. The same inputs always give the same bucket, in every process and tier, and buckets are

uniform to within 0.003%.

split_variant fn

fn split_variant(weights: List[(Str, Int)], b: Int where _ >= 0 and _ < 100000) -> Opt[Str]

The variant a split gives to bucket b (0 to 99999): weights are laid out in order over the buckets, in proportion. None when every weight is 0.

Reason type

pub type Reason = Static | Default | TargetingMatch | Split | Disabled | PrerequisiteFailed | ErrorReason derive Eq, Show

Why a flag has the value it has, as in OpenFeature: Static (the flag has no targets or rules), Default (nothing matched; the fallthrough variant), TargetingMatch (a target or rule matched), Split (a weighted split chose), Disabled (the flag is off), PrerequisiteFailed (a prerequisite did not hold; the off variant), or ErrorReason (see the error code; the caller's default is used).

reason_name fn

fn reason_name(r: Reason) -> Str

The OpenFeature spelling of a reason: STATIC, DEFAULT, TARGETING_MATCH, SPLIT, DISABLED, PREREQUISITE_FAILED or ERROR.

ErrorCode type

pub type ErrorCode = FlagNotFound | TypeMismatch | ParseError | TargetingKeyMissing | InvalidContext | General derive Eq, Show

What went wrong, as in OpenFeature: no such flag, a typed accessor for another type, a split without its bucketing attribute, or a definition that refers to something missing or forms a prerequisite cycle (General, with a message). ParseError and InvalidContext complete the OpenFeature set for code that wraps this package (definitions that failed to load, a context it rejects); evaluation itself never produces them.

error_name fn

fn error_name(e: ErrorCode) -> Str

The OpenFeature spelling of an error code: FLAG_NOT_FOUND, TYPE_MISMATCH, PARSE_ERROR, TARGETING_KEY_MISSING, INVALID_CONTEXT or GENERAL.

Details type

pub type Details = {key: Str, value: Opt[FlagValue], variant: Opt[Str], reason: Reason, error: Opt[ErrorCode], message: Str, rule: Opt[Str], flag_version: Int} derive Eq, Show

The result of evaluating a flag: the value and variant (none on an error), why, the error code and a message on an error, the id of the rule that matched (target:VARIANT for an explicit target), and the flag's version.

clause_holds fn

fn clause_holds(s: Snapshot, c: Clause, ctx: Context) -> Bool

Whether c holds for ctx, with the snapshot's segments. A clause naming a missing segment is false (validation reports it).

in_segment fn

fn in_segment(s: Snapshot, seg: Segment, ctx: Context) -> Bool

Whether ctx belongs to segment seg: not excluded, and included or matched by a rule. A segment rule that names a segment is false (validation reports it).

evaluate fn

fn evaluate(s: Snapshot, key: Str, ctx: Context) -> Details

Evaluates flag key for ctx. Pure: the same snapshot and context always give the same details.

Resolved type

pub type Resolved[T] = {value: T, details: Details} derive Eq, Show

A typed result: the value (the caller's default on any error) and the details.

bool_details fn

fn bool_details(s: Snapshot, key: Str, ctx: Context, default: Bool) -> Resolved[Bool]

A boolean flag's value and details; default on any error, including TypeMismatch.

str_details fn

fn str_details(s: Snapshot, key: Str, ctx: Context, default: Str) -> Resolved[Str]

A string flag's value and details; default on any error.

int_details fn

fn int_details(s: Snapshot, key: Str, ctx: Context, default: Int) -> Resolved[Int]

An integer flag's value and details; default on any error.

num_details fn

fn num_details(s: Snapshot, key: Str, ctx: Context, default: F64) -> Resolved[F64]

A number flag's value and details (integer variants are accepted as numbers); default on any error.

is_on fn

fn is_on(s: Snapshot, key: Str, ctx: Context, default: Bool) -> Bool

Whether boolean flag key is on for ctx; default on any error.

str_value fn

fn str_value(s: Snapshot, key: Str, ctx: Context, default: Str) -> Str

A string flag's value for ctx; default on any error.

int_value fn

fn int_value(s: Snapshot, key: Str, ctx: Context, default: Int) -> Int

An integer flag's value for ctx; default on any error.

num_value fn

fn num_value(s: Snapshot, key: Str, ctx: Context, default: F64) -> F64

A number flag's value for ctx; default on any error.

evaluate_all fn

fn evaluate_all(s: Snapshot, ctx: Context) -> List[Details]

Every flag of the snapshot evaluated for ctx, in key order: what a client-side SDK bootstrap needs.

problems fn

fn problems(s: Snapshot) -> List[Str]

Every problem in a snapshot, each with its path (flags.KEY.rules[0].when.values: ...): unknown variants, segments and prerequisite flags, prerequisite cycles, operator arities, numbers and times that don't parse, bad regular expressions, splits without positive weights, variants of mixed types, duplicate names and keys that aren't allowed. Empty when the snapshot is valid.

VariantJson type

pub type VariantJson = {name: Str, boolean: Opt[Bool], string: Opt[Str], integer: Opt[Int], number: Opt[F64]} derive Eq, Show

A variant on the wire: its name and exactly one of boolean, string, integer, number.

ClauseJson type

pub type ClauseJson = {attr: Opt[Str], op: Opt[Str], values: Opt[List[Str]], segment: Opt[Str], all: Opt[List[ClauseJson]], any: Opt[List[ClauseJson]], negate: Opt[ClauseJson], always: Opt[Bool]} derive Eq, Show

A clause on the wire: exactly one of an attribute comparison (attr, op, values), segment, all, any or negate; {} with always: true always holds.

WeightJson type

pub type WeightJson = {variant: Str, weight: Int} derive Eq, Show

A weighted variant of a split on the wire.

ServeJson type

pub type ServeJson = {variant: Opt[Str], split: Opt[List[WeightJson]], bucket_by: Opt[Str]} derive Eq, Show

A serve on the wire: variant, or split (with an optional bucket_by, default key).

RuleJson type

pub type RuleJson = {id: Str, when: ClauseJson, serve: ServeJson} derive Eq, Show

A rule on the wire.

FlagJson type

pub type FlagJson = {key: Str, enabled: Opt[Bool], variants: List[VariantJson], off_variant: Str, fallthrough: ServeJson, targets: Opt[List[Target]], rules: Opt[List[RuleJson]], prerequisites: Opt[List[Prereq]], salt: Opt[Str], version: Opt[Int]} derive Eq, Show

A flag on the wire. Missing targets, rules, prerequisites, salt and version are empty (0); a missing enabled is true.

SegmentJson type

pub type SegmentJson = {key: Str, included: Opt[List[Str]], excluded: Opt[List[Str]], rules: Opt[List[ClauseJson]]} derive Eq, Show

A segment on the wire.

DefinitionsJson type

pub type DefinitionsJson = {flags: List[FlagJson], segments: Opt[List[SegmentJson]]} derive Eq, Show

A definitions document: {"flags": [...], "segments": [...]}.

op_name fn

fn op_name(op: Op) -> Str

The name of an operator in the JSON format (equals, not_in, starts_with, lt, ...).

parse_op fn

fn parse_op(name: Str) -> Opt[Op]

The operator with this JSON name.

decode fn

fn decode(text: Str) -> Res[Snapshot, Str]

The snapshot a definitions document describes, validated (see problems). Errors name the path: flags[1].rules[0].when.op: unknown operator 'eqals'. Flags and segments must have unique keys.

encode fn

fn encode(s: Snapshot) -> Str

The definitions document of a snapshot; decode reads it back to the same flags and segments.

encode_flag fn

fn encode_flag(f: Flag) -> Str

One flag as JSON in the definitions format.

encode_segment fn

fn encode_segment(s: Segment) -> Str

One segment as JSON in the definitions format.

decode_flag fn

fn decode_flag(text: Str) -> Res[Flag, Str]

A flag from its JSON (one element of flags), checked on its own: references to other flags and segments are checked when it joins a snapshot.

decode_segment fn

fn decode_segment(text: Str) -> Res[Segment, Str]

A segment from its JSON (one element of segments).

load_file fn fs

fn load_file(path: Str) -> Res[Snapshot, Str] ! fs

The definitions in a JSON file, validated.

fingerprint fn

fn fingerprint(s: Snapshot) -> Str

A fingerprint of a snapshot's definitions (the SHA-256 of their JSON, in hex): equal snapshots have equal fingerprints, so a poller can tell whether anything changed.

StoreErr type

pub type StoreErr = Invalid{msg: Str} | Conflict{key: Str, expected: Int, actual: Int} | NotFound{key: Str} derive Eq, Show

Why a store change was refused: the change would make the definitions invalid, the flag or segment changed since the version the caller read (expected), or it does not exist. The variant names answer 400, 409 and 404 when an http.serve handler raises them.

AuditEntry type

pub type AuditEntry = {id: Int, kind: Str, key: Str, action: Str, definition: Opt[Str], version: Int, actor: Str, at: Time} derive Eq, Show

One entry of the audit history: what changed (flag or segment, its key), the action (put or delete), the definition after a put, the store version it produced, who did it and when.

open_store fn db

fn open_store(c: Db) -> Unit ! db, fail[DbErr]

Creates the store's tables if they are missing (featureflags_flags, featureflags_segments, featureflags_audit, featureflags_meta). Safe to call on every start.

store_version fn db

fn store_version(c: Db) -> Int ! db, fail[DbErr]

The store's version: it increases by one with every change, so comparing it with a snapshot's version is the cheap way to tell whether to reload.

load_store fn db

fn load_store(c: Db) -> Snapshot ! db, fail[DbErr], fail[StoreErr]

Every flag and segment in the store, as a snapshot with the store's version. Read in one transaction, so it is consistent.

reload_if_changed fn db

fn reload_if_changed(c: Db, have: Int) -> Opt[Snapshot] ! db, fail[DbErr], fail[StoreErr]

The store's snapshot if its version differs from have, else none: a poll costs one indexed read when nothing changed.

put_flag fn db

fn put_flag(c: Db, f: Flag, expected: Opt[Int], actor: Str, at: Time) -> Flag ! db, fail[DbErr], fail[StoreErr]

Creates or replaces flag f, in a transaction: the whole store must stay valid with it. With expected (the flag's version the caller read, 0 for a new flag) a concurrent change is a Conflict instead of being overwritten. Returns the flag as stored, with its new version.

delete_flag fn db

fn delete_flag(c: Db, key: Str, expected: Opt[Int], actor: Str, at: Time) -> Unit ! db, fail[DbErr], fail[StoreErr]

Deletes flag key; refused while another flag has it as a prerequisite.

put_segment fn db

fn put_segment(c: Db, g: Segment, expected: Opt[Int], actor: Str, at: Time) -> Int ! db, fail[DbErr], fail[StoreErr]

Creates or replaces segment g; the store must stay valid with it.

delete_segment fn db

fn delete_segment(c: Db, key: Str, expected: Opt[Int], actor: Str, at: Time) -> Unit ! db, fail[DbErr], fail[StoreErr]

Deletes segment key; refused while a flag uses it.

import_snapshot fn db

fn import_snapshot(c: Db, s: Snapshot, actor: Str, at: Time) -> Int ! db, fail[DbErr], fail[StoreErr]

Replaces everything in the store with the snapshot's flags and segments (keeping each flag's version counting), in one transaction, with one audit entry per change. Returns the new store version.

history fn db

fn history(c: Db, kind: Str, key: Str) -> List[AuditEntry] ! db, fail[DbErr]

The audit history of one flag or segment (kind is flag or segment), oldest first.

changes_since fn db

fn changes_since(c: Db, since: Int) -> List[AuditEntry] ! db, fail[DbErr]

The audit entries after store version since, oldest first: what changed since a snapshot.

evaluate_stored fn db

fn evaluate_stored(c: Db, key: Str, ctx: Context) -> Details ! db, fail[DbErr], fail[StoreErr]

Evaluates flag key straight from the store, reading only that flag, its prerequisites and the segments they use: cheap enough for every request of a handler that shares the Db.

Versions

VersionPublishedCommitSourceEffects
0.1.0 2026-10-16a4cd55744esnapshotdb fs