@voltro/plugin-flags 0.33.0 → 0.35.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/CHANGELOG.md +1968 -0
- package/dist/define-D9Eet7wA.js +79 -0
- package/dist/index.d.ts +552 -1
- package/dist/index.js +441 -120
- package/dist/rpc.js +4 -2
- package/dist/web.d.ts +160 -0
- package/dist/web.js +8 -7
- package/package.json +5 -5
package/dist/rpc.js
CHANGED
|
@@ -7,7 +7,8 @@ var n = t({
|
|
|
7
7
|
output: e.Record({
|
|
8
8
|
key: e.String,
|
|
9
9
|
value: e.Boolean
|
|
10
|
-
})
|
|
10
|
+
}),
|
|
11
|
+
openAccess: "per-caller flag evaluation: returns only the values computed for the calling subject; every session (incl. anonymous) evaluates its own flags"
|
|
11
12
|
}), r = e.Struct({
|
|
12
13
|
name: e.String,
|
|
13
14
|
value: e.Unknown,
|
|
@@ -18,7 +19,8 @@ var n = t({
|
|
|
18
19
|
output: e.Record({
|
|
19
20
|
key: e.String,
|
|
20
21
|
value: r
|
|
21
|
-
})
|
|
22
|
+
}),
|
|
23
|
+
openAccess: "per-caller variant resolution: returns only the variants served to the calling subject; every session (incl. anonymous) evaluates its own flags"
|
|
22
24
|
}), a = [{
|
|
23
25
|
tag: "flags.evaluate",
|
|
24
26
|
kind: "query",
|
package/dist/web.d.ts
CHANGED
|
@@ -1,7 +1,90 @@
|
|
|
1
|
+
/** The result of decoding an UNTRUSTED value (a DB override, a dashboard edit)
|
|
2
|
+
* against a flag's Schema. Deliberately not an exception: the caller's correct
|
|
3
|
+
* response is to keep the code-declared value and say so, not to unwind. */
|
|
4
|
+
declare type FlagDecodeResult<A> = {
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
readonly value: A;
|
|
7
|
+
} | {
|
|
8
|
+
readonly ok: false;
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare interface FlagDefinition {
|
|
13
|
+
/** Master switch. `false` → off for everyone regardless of rollout/targeting. Default true. */
|
|
14
|
+
readonly enabled?: boolean;
|
|
15
|
+
/** Percentage rollout 0..100 — deterministic per (key, subject/tenant) so a
|
|
16
|
+
* caller stays in/out consistently. Omit = 100 (everyone the targeting allows). */
|
|
17
|
+
readonly rollout?: number;
|
|
18
|
+
/** OR-of-rules targeting. When present, the caller must match at least one
|
|
19
|
+
* rule (in addition to passing the rollout). Omit = everyone. */
|
|
20
|
+
readonly targeting?: ReadonlyArray<FlagTargetingRule>;
|
|
21
|
+
/** Bucket on the tenant id instead of the subject id (rollout by tenant). */
|
|
22
|
+
readonly rolloutBy?: 'subject' | 'tenant';
|
|
23
|
+
/** Multivariate flag — a set of named variants the flag resolves ONE of per
|
|
24
|
+
* subject, deterministically weighted (see `FlagVariant.weight`). A subject
|
|
25
|
+
* stays in the same variant across calls. When present, `variant.evaluate`
|
|
26
|
+
* serves the resolved variant; the boolean resolution is `enabled && variant
|
|
27
|
+
* is served` (the flag is "on" for the caller unless a variant named in
|
|
28
|
+
* `offVariant` is served — omit ⇒ any served variant counts as on). */
|
|
29
|
+
readonly variants?: ReadonlyArray<FlagVariant>;
|
|
30
|
+
/** Variant name treated as the "off" state for boolean resolution. Omit ⇒
|
|
31
|
+
* passing targeting+rollout with any served variant counts as on. */
|
|
32
|
+
readonly offVariant?: string;
|
|
33
|
+
/** Time-boxed / ramping schedule, evaluated against the current time. */
|
|
34
|
+
readonly schedule?: FlagSchedule;
|
|
35
|
+
/** Surfaced in the dashboard / `flags.evaluate`. */
|
|
36
|
+
readonly description?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A time-boxed / ramping schedule. Evaluated against the current time.
|
|
40
|
+
* Order: outside [activateAt, deactivateAt) ⇒ off; a `ramp` interpolates the
|
|
41
|
+
* effective rollout % linearly across its window. */
|
|
42
|
+
declare interface FlagSchedule {
|
|
43
|
+
/** Flag is off before this instant (epoch millis or ISO string). Omit ⇒ no lower bound. */
|
|
44
|
+
readonly activateAt?: number | string;
|
|
45
|
+
/** Flag is off at/after this instant (epoch millis or ISO string). Omit ⇒ no upper bound. */
|
|
46
|
+
readonly deactivateAt?: number | string;
|
|
47
|
+
/** A ramping rollout: exposure grows linearly from `from`% to `to`% across
|
|
48
|
+
* [`startAt`, `endAt`). Before `startAt` ⇒ `from`%, after `endAt` ⇒ `to`%.
|
|
49
|
+
* The interpolated % REPLACES the flag's static `rollout` while active. */
|
|
50
|
+
readonly ramp?: {
|
|
51
|
+
readonly from: number;
|
|
52
|
+
readonly to: number;
|
|
53
|
+
readonly startAt: number | string;
|
|
54
|
+
readonly endAt: number | string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
1
58
|
export declare type FlagSet = Readonly<Record<string, boolean>>;
|
|
2
59
|
|
|
60
|
+
/** One targeting rule — ANDs its conditions; a flag's `targeting` is an OR of rules. */
|
|
61
|
+
declare interface FlagTargetingRule {
|
|
62
|
+
/** Match specific subject ids. */
|
|
63
|
+
readonly subjectIds?: ReadonlyArray<string>;
|
|
64
|
+
/** Match specific tenant ids. */
|
|
65
|
+
readonly tenantIds?: ReadonlyArray<string>;
|
|
66
|
+
/** Match `subject.type` (user / apiKey / serviceAccount / anonymous). */
|
|
67
|
+
readonly subjectTypes?: ReadonlyArray<string>;
|
|
68
|
+
/** Match `subject.metadata[key] === value` for every entry. */
|
|
69
|
+
readonly metadata?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One multivariate variant: a named value with an optional rollout weight. */
|
|
73
|
+
declare interface FlagVariant {
|
|
74
|
+
/** Stable variant name (the resolution returns this). */
|
|
75
|
+
readonly name: string;
|
|
76
|
+
/** The value served for this variant (string / number / boolean / JSON). */
|
|
77
|
+
readonly value: FlagVariantValue;
|
|
78
|
+
/** Relative allocation weight. Omit ⇒ equal split of the remaining weight.
|
|
79
|
+
* Weights are normalised, so `[3, 1]` is 75% / 25%. */
|
|
80
|
+
readonly weight?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
3
83
|
export declare type FlagVariantSet = Readonly<Record<string, FlagVariantView>>;
|
|
4
84
|
|
|
85
|
+
/** A concrete variant value — string / number / boolean / JSON. Kept browser-safe. */
|
|
86
|
+
declare type FlagVariantValue = string | number | boolean | null | ReadonlyArray<unknown> | Record<string, unknown>;
|
|
87
|
+
|
|
5
88
|
/** A resolved multivariate variant on the wire: name + value + on/off. */
|
|
6
89
|
export declare interface FlagVariantView {
|
|
7
90
|
readonly name: string;
|
|
@@ -9,6 +92,63 @@ export declare interface FlagVariantView {
|
|
|
9
92
|
readonly enabled: boolean;
|
|
10
93
|
}
|
|
11
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A flag with a value type.
|
|
97
|
+
*
|
|
98
|
+
* `out A` is load-bearing rather than decorative: the plugin holds these in one
|
|
99
|
+
* `ReadonlyArray<TypedFlag<unknown>>`, which is only sound while every member
|
|
100
|
+
* that mentions `A` is a read position. If you add a field that CONSUMES an `A`
|
|
101
|
+
* (a `(value: A) => …` callback, say), that annotation is what will stop you —
|
|
102
|
+
* and the fix is to erase it behind a function returning a result, the way
|
|
103
|
+
* `decode` already does, not to drop the annotation.
|
|
104
|
+
*/
|
|
105
|
+
declare interface TypedFlag<out A> {
|
|
106
|
+
/** Brand — lets the plugin discriminate a typed flag from a bare `FlagValue`. */
|
|
107
|
+
readonly _voltroTypedFlag: true;
|
|
108
|
+
/** The flag key. Shares the flag namespace with `flags: { … }` entries; a
|
|
109
|
+
* duplicate is refused at plugin construction. */
|
|
110
|
+
readonly key: string;
|
|
111
|
+
/** Human label of the value Schema (`number`, `"a" | "b"`, …) — for the
|
|
112
|
+
* dashboard panel and for the refusal message when an override fails to
|
|
113
|
+
* decode. Derived from the Schema's AST, so it cannot drift from it. */
|
|
114
|
+
readonly valueType: string;
|
|
115
|
+
/** The value served whenever the flag does not resolve to a variant: it is
|
|
116
|
+
* off for this caller, or it declares no variants at all. Compile-checked
|
|
117
|
+
* against the Schema. */
|
|
118
|
+
readonly defaultValue: A;
|
|
119
|
+
/** The value arms. Empty ⇒ the flag always serves `defaultValue` (which the
|
|
120
|
+
* lifecycle report classifies as `constantOn`/`constantOff` — a flag that
|
|
121
|
+
* cannot serve anything else is one to delete). */
|
|
122
|
+
readonly variants: ReadonlyArray<TypedFlagVariant<A>>;
|
|
123
|
+
/** Decode an untrusted value against the flag's Schema — the runtime half of
|
|
124
|
+
* the contract `default` gets at compile time. */
|
|
125
|
+
readonly decode: (input: unknown) => FlagDecodeResult<A>;
|
|
126
|
+
/** The evaluator-facing definition. This is what goes into the registry the
|
|
127
|
+
* existing `evaluateFlag` / `resolveVariant` already read; nothing about
|
|
128
|
+
* evaluation changes because a flag is typed. */
|
|
129
|
+
readonly definition: FlagDefinition;
|
|
130
|
+
/**
|
|
131
|
+
* IN-11 — the `defineExperiment` this flag's variants report uplift for.
|
|
132
|
+
*
|
|
133
|
+
* A NAME rather than the definition object, on purpose: `defineExperiment`
|
|
134
|
+
* lives in `@voltro/runtime`, which is server-only, and this module has to
|
|
135
|
+
* stay loadable in a browser bundle. The link is validated at BOOT, where
|
|
136
|
+
* both sides are known (`assertFlagExperimentLinkage`) — a flag naming an
|
|
137
|
+
* experiment that does not exist, or whose arms disagree with the flag's,
|
|
138
|
+
* refuses to start rather than reporting uplift for arms nobody is served.
|
|
139
|
+
*/
|
|
140
|
+
readonly experiment?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One named arm of a typed flag. `value` is checked against the flag's Schema
|
|
144
|
+
* at COMPILE time. Weights behave exactly as `FlagVariant.weight`. */
|
|
145
|
+
declare interface TypedFlagVariant<out A> {
|
|
146
|
+
readonly name: string;
|
|
147
|
+
readonly value: A;
|
|
148
|
+
/** Relative allocation weight. Omit ⇒ equal split. */
|
|
149
|
+
readonly weight?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
12
152
|
/** `true` iff a single flag is on for the caller. */
|
|
13
153
|
export declare const useFlag: (key: string, apiName?: string) => boolean;
|
|
14
154
|
|
|
@@ -16,6 +156,26 @@ export declare const useFlag: (key: string, apiName?: string) => boolean;
|
|
|
16
156
|
* on reconnect. Returns `{}` until the first response. */
|
|
17
157
|
export declare const useFlags: (apiName?: string) => FlagSet;
|
|
18
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The TYPED value of a `defineFlag()` flag — `A`, not `unknown`.
|
|
161
|
+
*
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { checkoutButton } from '../lib/flags'
|
|
164
|
+
* const colour = useFlagValue(checkoutButton) // 'blue' | 'green'
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* The flag object is the SAME one `app.config.ts` registers (`defineFlag` is
|
|
168
|
+
* browser-safe by construction), so the type the server serves and the type the
|
|
169
|
+
* component reads cannot drift — and neither can the fallback: `typedValueOf`
|
|
170
|
+
* is the one resolution both sides use.
|
|
171
|
+
*
|
|
172
|
+
* Before the first response, and for a value the server sent that does not
|
|
173
|
+
* decode, the declared `default` is returned. That branch is real rather than
|
|
174
|
+
* theoretical: an older replica mid-deploy can still be serving a variant set a
|
|
175
|
+
* newer schema has narrowed.
|
|
176
|
+
*/
|
|
177
|
+
export declare const useFlagValue: <A>(flag: TypedFlag<A>, apiName?: string) => A;
|
|
178
|
+
|
|
19
179
|
/** The served variant for a single multivariate flag (or `undefined` until the
|
|
20
180
|
* first response / for an unknown key). Read `.value` for the variant payload. */
|
|
21
181
|
export declare const useVariant: (key: string, apiName?: string) => FlagVariantView | undefined;
|
package/dist/web.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as e } from "./define-D9Eet7wA.js";
|
|
2
|
+
import { useSubscription as t } from "@voltro/client";
|
|
2
3
|
//#region src/web.ts
|
|
3
|
-
var
|
|
4
|
-
let { data: n } = e
|
|
4
|
+
var n = (e = "app") => {
|
|
5
|
+
let { data: n } = t(e, "flags.evaluate", {});
|
|
5
6
|
return n ?? {};
|
|
6
|
-
},
|
|
7
|
-
let { data: n } = e
|
|
7
|
+
}, r = (e, t = "app") => n(t)[e] === !0, i = (e = "app") => {
|
|
8
|
+
let { data: n } = t(e, "flags.variants", {});
|
|
8
9
|
return n ?? {};
|
|
9
|
-
},
|
|
10
|
+
}, a = (e, t = "app") => i(t)[e], o = (t, n = "app") => e(t, i(n)[t.key]);
|
|
10
11
|
//#endregion
|
|
11
|
-
export {
|
|
12
|
+
export { r as useFlag, o as useFlagValue, n as useFlags, a as useVariant, i as useVariants };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-flags",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Feature flags — per-subject / per-tenant targeting, deterministic % rollouts, kill-switch. Gate mutations/queries/actions declaratively or guard in-handler; evaluate flags client-side for UI gating. Config-as-code (memory) or runtime-toggleable (postgres).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/client": "0.
|
|
52
|
-
"@voltro/database": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/protocol": "0.
|
|
51
|
+
"@voltro/client": "0.35.0",
|
|
52
|
+
"@voltro/database": "0.35.0",
|
|
53
|
+
"@voltro/logger": "0.35.0",
|
|
54
|
+
"@voltro/protocol": "0.35.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"effect": "^3.22.0"
|