ZigBase 0.8.0
·
873 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Breaking
- Feature flags are now declared-only. Flags must be declared in the
App(.{ .flags = .{ … } })literal; only declared flags resolve. The v0.7 runtime-string APIctx.flag("arbitrary")(KV-or-false) has been removed — use the typedApp.flag(ctx, .name)for known flags, orctx.flagByName("name")(returns?bool, null when undeclared) for dynamic names. ctx.setFlagnow writes a declared-flag override. It writes theflag:<name>override key for a DECLARED flag and errorserror.UndeclaredFlagotherwise (the typed, compile-checked form isApp.setFlag(ctx, .name, enabled)). Previously it set an arbitrary<name>KV value.
Features
- Comptime feature-flag + experiment registry (#128/#129/#130). Declare
.flags(bare-bool default or.{ .default, .description }) and.experiments(.{ .variants, .weights, .sticky, .description }) in theApp(cfg)literal. Malformed declarations (unknown sub-key, non-bool flag, variants/weights length mismatch, empty/duplicate variants, all-zero weights) are loud@compileErrors. - Typed, compile-checked accessors.
App.flag(ctx, .name) bool,App.setFlag(ctx, .name, enabled) !void, andApp.experiment(ctx, .name, subject) ![]const u8— a typo'd flag/experiment name is a compile error (generatedApp.Flag/App.Experimentenums). - Runtime resolution.
ctx.flagByName(name) ?bool(dynamic read),ctx.flags().resolveAll(subject)resolves every declared flag + experiment in a single batched_kvscan, and deterministic experiment bucketing (FNV1a-64(name ++ 0x00 ++ subject)over cumulative weights) gives a stable variant per(name, subject). Per-flag overrides live in_kvunderflag:<name>; experiment weight overrides underexp:<name>:weights(JSON). - Admin UI gains a Feature Flags & Experiments screen (
/_/#/features) showing every declared flag (name, default, description, effective value) with a toggle to set/clear theflag:<name>override, and each declared experiment's variants with editable weight sliders that write theexp:<name>:weightsoverride; a "Reset to declared" action clears the override. Superuser-only; backed by the newGET /api/featuresendpoint. - New
GET /api/featuresendpoint (superuser) returns the comptime-declared flag + experiment registry alongside each entry's current_kvoverride — useful for custom admin tooling. - Feature exposure events: register
.onFeatureExposureto receive anExposureEvent({ kind: .flag | .experiment, name, subject, value, variant }) each time a declared flag or experiment is resolved. The hook is notify-only and zero-cost when unregistered (the resolver never builds the event without a handler). - Realtime feature signal: any flag/experiment override change (
ctx.setFlag/App.setFlagor an adminPUT/DELETEof aflag:<name>/exp:<name>:weightssetting) broadcasts a signal-only{"type":"features.changed"}frame on the public__featureschannel. Clients may subscribe anonymously and re-GET /api/stateon receipt; no per-subject state or experiment assignment is ever pushed over the socket. - Public feature-state endpoint (#130).
GET /api/state?subject=<id>is an unauthenticated, read-only projection of resolved flags + experiments:{ "flags": { "<name>": <bool>, … }, "experiments": { "<name>": "<variant>", … } }. It exposes resolved values ONLY — never the_kvkeys, defaults, weights, timestamps, or any superuser settings verb (those stay behindrequireSuperuser). A.stickyexperiment returns its persisted assignment here too (agreeing withApp.experiment), resolved reader-first so a caller-supplied subject can't storm the writer lock. Auto-mounts at/api/state; configure with.features = .{ .public_route = "/state" }to remap or.{ .public_route = .disabled }to turn off. - Typed
zb.flags.resolveAll(subject)in the TypeScript SDK.zig build gen-clientnow emits a fully-typed feature-state surface from yourApp(.{ .flags, .experiments }): flags as namedbooleans and each experiment as a string-literal union of its declared variants (FeatureState).await zb.flags.resolveAll("user-42")callsGET /api/stateand returns{ flags: { … }, experiments: { … } }with noany. Emitted only when flags/experiments are declared; the runtime-introspection tier omits it (no comptime metadata), matching typed routes and custom auth methods. - Sticky experiment assignments (#129): declare an experiment
.sticky = trueto persist a subject's first variant in_experiment_assignmentsso it survives later weight changes (new subjects still follow the current weights; empty subjects are never persisted). A framework-internal_experiment_gcjob — installed only when a.stickyexperiment is declared — reaps assignments older than the new.experiment_assignment_ttlconfig (in days, default90) hourly in bounded batches.