@voltro/plugin-flags 0.85.0 → 0.87.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 +5987 -0
- package/README.md +106 -0
- package/THIRD-PARTY-NOTICES.md +1125 -34
- package/dist/config-inspect.d.ts +33 -0
- package/dist/config-inspect.js +29 -0
- package/dist/config-port.d.ts +247 -0
- package/dist/config-port.js +125 -0
- package/dist/config-testing.d.ts +336 -0
- package/dist/config-testing.js +114 -0
- package/dist/config.d.ts +250 -0
- package/dist/config.js +235 -0
- package/dist/{define-BHpLJh1F.js → define.js} +1 -1
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +15 -2
- package/dist/evaluate.js +81 -0
- package/dist/experimentLink.js +48 -0
- package/dist/index.d.ts +164 -1
- package/dist/index.js +330 -609
- package/dist/lifecycle.js +82 -0
- package/dist/rpc.d.ts +14 -0
- package/dist/rpc.js +29 -14
- package/dist/store.js +190 -0
- package/dist/usage.js +113 -0
- package/dist/web.d.ts +24 -0
- package/dist/web.js +15 -9
- package/package.json +26 -6
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { toDefinition as e } from "./evaluate.js";
|
|
2
|
+
//#region src/lifecycle.ts
|
|
3
|
+
var t = 864e5, n = (e) => typeof e == "number" ? e : Date.parse(e), r = (t, r) => {
|
|
4
|
+
let i = e(t);
|
|
5
|
+
if (i.enabled === !1) return {
|
|
6
|
+
shape: "constantOff",
|
|
7
|
+
reason: "enabled: false — resolves false for every caller"
|
|
8
|
+
};
|
|
9
|
+
if (i.schedule) {
|
|
10
|
+
let { activateAt: e, deactivateAt: t } = i.schedule;
|
|
11
|
+
if (t != null && r >= n(t)) return {
|
|
12
|
+
shape: "expired",
|
|
13
|
+
reason: `schedule.deactivateAt (${String(t)}) has passed — the flag can never be on again`
|
|
14
|
+
};
|
|
15
|
+
if (e != null && r < n(e)) return {
|
|
16
|
+
shape: "notYetActive",
|
|
17
|
+
reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
let a = i.rollout ?? 100;
|
|
21
|
+
if (a <= 0) return {
|
|
22
|
+
shape: "constantOff",
|
|
23
|
+
reason: `rollout: ${a} — nobody is in the rollout`
|
|
24
|
+
};
|
|
25
|
+
let o = (i.targeting?.length ?? 0) > 0, s = (i.variants?.length ?? 0) > 1, c = i.schedule !== void 0 && (i.schedule.ramp !== void 0 || i.schedule.deactivateAt != null && r < n(i.schedule.deactivateAt));
|
|
26
|
+
return a >= 100 && !o && !s && !c ? {
|
|
27
|
+
shape: "constantOn",
|
|
28
|
+
reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
|
|
29
|
+
} : {
|
|
30
|
+
shape: "conditional",
|
|
31
|
+
reason: `still selects between callers (${[
|
|
32
|
+
a < 100 ? `rollout ${a}%` : void 0,
|
|
33
|
+
o ? `${i.targeting?.length} targeting rule(s)` : void 0,
|
|
34
|
+
s ? `${i.variants?.length} variants` : void 0,
|
|
35
|
+
c ? "a live schedule" : void 0
|
|
36
|
+
].filter((e) => e !== void 0).join(", ")})`
|
|
37
|
+
};
|
|
38
|
+
}, i = [
|
|
39
|
+
"Reachability is not decided. \"Not evaluated since <date>\" is a measurement; \"this code path is dead\" is not decidable in general. A seasonal flag, a flag behind a route nobody visited this month, and a flag whose last call site was deleted are indistinguishable from here.",
|
|
40
|
+
"Only SERVER-side reads are counted as use: isFlagEnabled / requireFlag / flagValue and a gatedBy interception. A flag consulted only in the browser via useFlag() / useFlagValue() reads from the bulk set the server already sent, so it never reaches this report as a named read.",
|
|
41
|
+
"A \"bulk\" observation means the flag was INCLUDED in a flags.evaluate / flags.variants response. It does not mean any client read that key — one useFlags() poll evaluates the whole registry.",
|
|
42
|
+
"The window is bounded by retention (see coverage.retentionDays). Nothing older is evidence: a flag last read BEFORE the window has no observation at all and reads \"neverObserved\", which is exactly what a flag declared this morning reads.",
|
|
43
|
+
"\"neverObserved\" is not a removal candidate. A flag declared yesterday and a flag abandoned last year produce the identical row; only a flag that WAS observed and then stopped (\"stale\") is proven unused."
|
|
44
|
+
], a = (e) => {
|
|
45
|
+
let n = e.now ?? Date.now(), a = e.staleAfterDays, o = n - a * t, s = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(), l;
|
|
46
|
+
for (let t of e.usage) {
|
|
47
|
+
let e = t.lastAt.getTime();
|
|
48
|
+
if (!Number.isFinite(e)) continue;
|
|
49
|
+
(l === void 0 || e < l) && (l = e);
|
|
50
|
+
let n = t.source === "bulk" ? c : s, r = n.get(t.flag);
|
|
51
|
+
(r === void 0 || e > r) && n.set(t.flag, e);
|
|
52
|
+
}
|
|
53
|
+
let u = l === void 0 ? 0 : Math.floor((n - l) / t);
|
|
54
|
+
return {
|
|
55
|
+
coverage: {
|
|
56
|
+
tracking: e.tracking,
|
|
57
|
+
observedSince: l === void 0 ? null : new Date(l).toISOString().slice(0, 10),
|
|
58
|
+
observedDays: u,
|
|
59
|
+
retentionDays: e.retentionDays,
|
|
60
|
+
staleAfterDays: a
|
|
61
|
+
},
|
|
62
|
+
findings: Object.entries(e.flags).map(([t, i]) => {
|
|
63
|
+
let { shape: l, reason: d } = r(i, n), f = s.get(t), p = c.get(t), m = [d], h;
|
|
64
|
+
e.tracking ? f !== void 0 && f >= o ? (h = "evaluated", m.push(`last server-side read ${new Date(f).toISOString()}`)) : f === void 0 ? (h = "neverObserved", m.push(u > 0 ? `no server-side read in the ${u}-day observation window` : "no server-side read observed yet (the observation window is empty)"), p !== void 0 && m.push("the flag WAS delivered to a client in a bulk flags.evaluate response — which does not prove it was read")) : (h = "stale", m.push(`last server-side read ${new Date(f).toISOString()}, more than ${a} day(s) ago, over an observation window of ${u} day(s)`)) : (h = "untracked", m.push("evaluation tracking is off — no usage evidence exists for this deployment"));
|
|
65
|
+
let g = l === "constantOn" || l === "constantOff" || l === "expired";
|
|
66
|
+
return g && m.push("the definition alone makes this flag a constant — it selects nothing"), {
|
|
67
|
+
key: t,
|
|
68
|
+
shape: l,
|
|
69
|
+
shapeReason: d,
|
|
70
|
+
usage: h,
|
|
71
|
+
lastTargetedAt: f === void 0 ? null : new Date(f).toISOString(),
|
|
72
|
+
lastBulkAt: p === void 0 ? null : new Date(p).toISOString(),
|
|
73
|
+
liveTargetedEvaluations: e.liveCounts?.get(t) ?? 0,
|
|
74
|
+
removalCandidate: g || h === "stale",
|
|
75
|
+
why: m
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
limits: i
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { i as FLAG_LIFECYCLE_LIMITS, r as classifyShape, a as flagLifecycleReport };
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PluginRpcClientDescriptor } from '@voltro/protocol';
|
|
2
2
|
import { QueryProcedureDescriptor } from '@voltro/protocol';
|
|
3
3
|
import { Schema } from 'effect';
|
|
4
|
+
import { YieldableError } from 'effect/Cause';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The plugin's canonical package name — the key an app's `alias` is recorded
|
|
@@ -14,6 +15,19 @@ import { Schema } from 'effect';
|
|
|
14
15
|
*/
|
|
15
16
|
export declare const BASE_NAME = "@voltro/plugin-flags";
|
|
16
17
|
|
|
18
|
+
/** Typed Config failures contain no private value, subject or decode body. */
|
|
19
|
+
declare class ConfigError extends ConfigError_base {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
|
|
23
|
+
readonly code: Schema.Literals<readonly ["invalid_input", "invalid_value", "not_found", "unavailable"]>;
|
|
24
|
+
}>, YieldableError>;
|
|
25
|
+
|
|
26
|
+
/** Only explicitly public Config keys, evaluated for the authenticated caller. */
|
|
27
|
+
export declare const configMineDescriptor: QueryProcedureDescriptor<"config.mine", Schema.Struct<{}>, Schema.$Record<Schema.String, Schema.Unknown>, typeof ConfigError>;
|
|
28
|
+
|
|
29
|
+
export declare const configRpcClientImports: readonly PluginRpcClientDescriptor[];
|
|
30
|
+
|
|
17
31
|
export declare const evaluateDescriptor: QueryProcedureDescriptor<"flags.evaluate", Schema.Struct<{}>, Schema.$Record<Schema.String, Schema.Boolean>, Schema.Never>;
|
|
18
32
|
|
|
19
33
|
export declare const evaluateVariantsDescriptor: QueryProcedureDescriptor<"flags.variants", Schema.Struct<{}>, Schema.$Record<Schema.String, Schema.Struct<{
|
package/dist/rpc.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ConfigError as e } from "./errors.js";
|
|
2
|
+
import { Schema as t } from "effect";
|
|
3
|
+
import { defineQuery as n } from "@voltro/protocol";
|
|
3
4
|
//#region src/rpc.ts
|
|
4
|
-
var
|
|
5
|
+
var r = "@voltro/plugin-flags", i = n({
|
|
5
6
|
name: "flags.evaluate",
|
|
6
|
-
input:
|
|
7
|
-
output:
|
|
7
|
+
input: t.Struct({}),
|
|
8
|
+
output: t.Record(t.String, t.Boolean),
|
|
8
9
|
openAccess: "per-caller flag evaluation: returns only the values computed for the calling subject; every session (incl. anonymous) evaluates its own flags"
|
|
9
|
-
}),
|
|
10
|
-
name:
|
|
11
|
-
value:
|
|
12
|
-
enabled:
|
|
13
|
-
}),
|
|
10
|
+
}), a = t.Struct({
|
|
11
|
+
name: t.String,
|
|
12
|
+
value: t.Unknown,
|
|
13
|
+
enabled: t.Boolean
|
|
14
|
+
}), o = n({
|
|
14
15
|
name: "flags.variants",
|
|
15
|
-
input:
|
|
16
|
-
output:
|
|
16
|
+
input: t.Struct({}),
|
|
17
|
+
output: t.Record(t.String, a),
|
|
17
18
|
openAccess: "per-caller variant resolution: returns only the variants served to the calling subject; every session (incl. anonymous) evaluates its own flags"
|
|
18
|
-
}),
|
|
19
|
+
}), s = [{
|
|
19
20
|
tag: "flags.evaluate",
|
|
20
21
|
kind: "query",
|
|
21
22
|
import: {
|
|
@@ -29,6 +30,20 @@ var n = "@voltro/plugin-flags", r = t({
|
|
|
29
30
|
module: "@voltro/plugin-flags/rpc",
|
|
30
31
|
name: "evaluateVariantsDescriptor"
|
|
31
32
|
}
|
|
33
|
+
}], c = n({
|
|
34
|
+
name: "config.mine",
|
|
35
|
+
input: t.Struct({}),
|
|
36
|
+
output: t.Record(t.String, t.Unknown),
|
|
37
|
+
error: e,
|
|
38
|
+
source: ["_voltro_feature_flags"],
|
|
39
|
+
openAccess: "Only declared public Config values computed from the actual calling subject; no subject selector or private keys."
|
|
40
|
+
}), l = [{
|
|
41
|
+
tag: "config.mine",
|
|
42
|
+
kind: "query",
|
|
43
|
+
import: {
|
|
44
|
+
module: "@voltro/plugin-flags/rpc",
|
|
45
|
+
name: "configMineDescriptor"
|
|
46
|
+
}
|
|
32
47
|
}];
|
|
33
48
|
//#endregion
|
|
34
|
-
export {
|
|
49
|
+
export { r as BASE_NAME, c as configMineDescriptor, l as configRpcClientImports, i as evaluateDescriptor, o as evaluateVariantsDescriptor, s as flagsRpcClientImports };
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { toDefinition as e } from "./evaluate.js";
|
|
2
|
+
import { boolean as t, derivedRowId as n, eq as r, id as i, integer as a, json as o, queryFor as s, table as c, text as l, timestamp as u } from "@voltro/database";
|
|
3
|
+
//#region src/store.ts
|
|
4
|
+
var d = "_voltro_feature_flag_audit", f = "_voltro_feature_flags", p = c(f, {
|
|
5
|
+
id: i({ prefix: "flag" }),
|
|
6
|
+
key: l().unique(),
|
|
7
|
+
enabled: t().default(!0),
|
|
8
|
+
rollout: a().nullable(),
|
|
9
|
+
rolloutBy: l().nullable(),
|
|
10
|
+
targeting: o().nullable(),
|
|
11
|
+
variants: o().nullable(),
|
|
12
|
+
offVariant: l().nullable(),
|
|
13
|
+
schedule: o().nullable(),
|
|
14
|
+
description: l().nullable(),
|
|
15
|
+
updatedAt: u().default("now").onUpdate("now")
|
|
16
|
+
}), m = c(d, {
|
|
17
|
+
id: i({ prefix: "flagaud" }),
|
|
18
|
+
flag: l(),
|
|
19
|
+
enabled: t(),
|
|
20
|
+
previousEnabled: t().nullable(),
|
|
21
|
+
actorId: l().nullable(),
|
|
22
|
+
value: o().nullable(),
|
|
23
|
+
previousValue: o().nullable(),
|
|
24
|
+
at: u().default("now")
|
|
25
|
+
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), h = (e, t) => {
|
|
26
|
+
let n = { ...e };
|
|
27
|
+
for (let e of t) {
|
|
28
|
+
let t = {
|
|
29
|
+
enabled: e.enabled,
|
|
30
|
+
...e.rollout == null ? {} : { rollout: e.rollout },
|
|
31
|
+
...e.rolloutBy === "tenant" || e.rolloutBy === "subject" ? { rolloutBy: e.rolloutBy } : {},
|
|
32
|
+
...Array.isArray(e.targeting) ? { targeting: e.targeting } : {},
|
|
33
|
+
...Array.isArray(e.variants) ? { variants: e.variants } : {},
|
|
34
|
+
...e.offVariant == null ? {} : { offVariant: e.offVariant },
|
|
35
|
+
...e.schedule != null && typeof e.schedule == "object" ? { schedule: e.schedule } : {},
|
|
36
|
+
...e.description == null ? {} : { description: e.description }
|
|
37
|
+
};
|
|
38
|
+
n[e.key] = t;
|
|
39
|
+
}
|
|
40
|
+
return n;
|
|
41
|
+
}, g = class {
|
|
42
|
+
flags = {};
|
|
43
|
+
set(e) {
|
|
44
|
+
this.flags = e;
|
|
45
|
+
}
|
|
46
|
+
all() {
|
|
47
|
+
return this.flags;
|
|
48
|
+
}
|
|
49
|
+
get(e) {
|
|
50
|
+
return this.flags[e];
|
|
51
|
+
}
|
|
52
|
+
}, _ = new g(), v = (e, t) => y(e, t) !== null, y = (t, n) => {
|
|
53
|
+
let r = _.get(t);
|
|
54
|
+
if (r === void 0) return null;
|
|
55
|
+
let i = e(r), a = i.enabled !== !1;
|
|
56
|
+
return _.set({
|
|
57
|
+
..._.all(),
|
|
58
|
+
[t]: {
|
|
59
|
+
...i,
|
|
60
|
+
enabled: n
|
|
61
|
+
}
|
|
62
|
+
}), a;
|
|
63
|
+
}, b = (t) => Object.entries(t).map(([t, n]) => {
|
|
64
|
+
let r = e(n);
|
|
65
|
+
return {
|
|
66
|
+
key: t,
|
|
67
|
+
enabled: r.enabled !== !1,
|
|
68
|
+
rollout: r.rollout ?? 100,
|
|
69
|
+
variants: r.variants && r.variants.length > 0 ? r.variants.map((e) => e.name) : void 0,
|
|
70
|
+
scheduled: r.schedule != null,
|
|
71
|
+
description: r.description
|
|
72
|
+
};
|
|
73
|
+
}), x = (t, n) => {
|
|
74
|
+
let r = e(n);
|
|
75
|
+
return {
|
|
76
|
+
key: t,
|
|
77
|
+
enabled: r.enabled !== !1,
|
|
78
|
+
rollout: r.rollout ?? null,
|
|
79
|
+
rolloutBy: r.rolloutBy ?? null,
|
|
80
|
+
targeting: r.targeting ?? null,
|
|
81
|
+
variants: r.variants ?? null,
|
|
82
|
+
offVariant: r.offVariant ?? null,
|
|
83
|
+
schedule: r.schedule ?? null,
|
|
84
|
+
description: r.description ?? null
|
|
85
|
+
};
|
|
86
|
+
}, S = (e) => ({
|
|
87
|
+
loadOverrides: async () => (await e.query(s(p).descriptor)).map((e) => ({
|
|
88
|
+
key: String(e.key),
|
|
89
|
+
enabled: e.enabled === !0 || e.enabled === 1,
|
|
90
|
+
rollout: e.rollout == null ? null : Number(e.rollout),
|
|
91
|
+
rolloutBy: e.rolloutBy ?? null,
|
|
92
|
+
targeting: e.targeting ?? null,
|
|
93
|
+
variants: e.variants ?? null,
|
|
94
|
+
offVariant: e.offVariant ?? null,
|
|
95
|
+
schedule: e.schedule ?? null,
|
|
96
|
+
description: e.description ?? null
|
|
97
|
+
})),
|
|
98
|
+
upsertOverride: async (t) => {
|
|
99
|
+
let n = C(t);
|
|
100
|
+
await e.upsert(f, n, { conflictColumns: ["key"] });
|
|
101
|
+
}
|
|
102
|
+
}), C = (e) => ({
|
|
103
|
+
id: n("flag", e.key),
|
|
104
|
+
key: e.key,
|
|
105
|
+
enabled: e.enabled,
|
|
106
|
+
rollout: e.rollout,
|
|
107
|
+
rolloutBy: e.rolloutBy,
|
|
108
|
+
targeting: e.targeting,
|
|
109
|
+
variants: e.variants,
|
|
110
|
+
offVariant: e.offVariant,
|
|
111
|
+
schedule: e.schedule,
|
|
112
|
+
description: e.description,
|
|
113
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
114
|
+
}), w = (e) => ({
|
|
115
|
+
flag: String(e.flag),
|
|
116
|
+
enabled: e.enabled === !0 || e.enabled === 1,
|
|
117
|
+
previousEnabled: e.previousEnabled == null ? null : e.previousEnabled === !0 || e.previousEnabled === 1,
|
|
118
|
+
actorId: e.actorId ?? null,
|
|
119
|
+
...e.value == null ? {} : { value: e.value },
|
|
120
|
+
...e.previousValue == null ? {} : { previousValue: e.previousValue },
|
|
121
|
+
at: e.at instanceof Date ? e.at : new Date(String(e.at))
|
|
122
|
+
}), T = (e) => ({
|
|
123
|
+
record: async (t) => {
|
|
124
|
+
await e.insert(d, {
|
|
125
|
+
id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
126
|
+
flag: t.flag,
|
|
127
|
+
enabled: t.enabled,
|
|
128
|
+
previousEnabled: t.previousEnabled,
|
|
129
|
+
actorId: t.actorId,
|
|
130
|
+
value: t.value ?? null,
|
|
131
|
+
previousValue: t.previousValue ?? null,
|
|
132
|
+
at: t.at
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
list: async (t) => {
|
|
136
|
+
let n = s(m).orderBy("at", "desc");
|
|
137
|
+
return t?.flag != null && (n = n.where(r("flag", t.flag))), t?.limit != null && (n = n.limit(t.limit)), (await e.query(n.descriptor)).map(w);
|
|
138
|
+
}
|
|
139
|
+
}), E = async (e, t, i, a, o) => {
|
|
140
|
+
await e.transactional(async (e) => {
|
|
141
|
+
await e.insertIgnore(f, {
|
|
142
|
+
...C(x(t, i)),
|
|
143
|
+
id: n("flag", crypto.randomUUID())
|
|
144
|
+
}, { conflictColumns: ["key"] });
|
|
145
|
+
let c = (await e.query(s(p).where(r("key", t)).forUpdate().descriptor))[0];
|
|
146
|
+
if (!c) throw Error("flag override disappeared during a Config change");
|
|
147
|
+
await e.update(f, String(c.id), {
|
|
148
|
+
enabled: !0,
|
|
149
|
+
rollout: 100,
|
|
150
|
+
rolloutBy: null,
|
|
151
|
+
targeting: null,
|
|
152
|
+
offVariant: null,
|
|
153
|
+
schedule: null,
|
|
154
|
+
variants: [{
|
|
155
|
+
name: "set",
|
|
156
|
+
value: a
|
|
157
|
+
}],
|
|
158
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
159
|
+
}), await T(e).record({
|
|
160
|
+
flag: t,
|
|
161
|
+
enabled: !0,
|
|
162
|
+
previousEnabled: c.enabled === !0 || c.enabled === 1,
|
|
163
|
+
actorId: o,
|
|
164
|
+
value: a,
|
|
165
|
+
previousValue: c.variants ?? null,
|
|
166
|
+
at: /* @__PURE__ */ new Date()
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}, D = async (e, t, i, a, o) => {
|
|
170
|
+
await e.transactional(async (e) => {
|
|
171
|
+
await e.insertIgnore(f, {
|
|
172
|
+
...C(x(t, i)),
|
|
173
|
+
id: n("flag", crypto.randomUUID())
|
|
174
|
+
}, { conflictColumns: ["key"] });
|
|
175
|
+
let c = (await e.query(s(p).where(r("key", t)).forUpdate().descriptor))[0];
|
|
176
|
+
if (!c) throw Error("flag override disappeared during toggle");
|
|
177
|
+
await e.update(f, String(c.id), {
|
|
178
|
+
enabled: a,
|
|
179
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
180
|
+
}), await T(e).record({
|
|
181
|
+
flag: t,
|
|
182
|
+
enabled: a,
|
|
183
|
+
previousEnabled: c.enabled === !0 || c.enabled === 1,
|
|
184
|
+
actorId: o,
|
|
185
|
+
at: /* @__PURE__ */ new Date()
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
export { d as FLAGS_AUDIT_TABLE, f as FLAGS_TABLE, g as FlagRegistry, T as dataStoreFlagAuditStore, S as dataStoreFlagStore, b as describeFlags, p as featureFlagsTable, m as flagAuditTable, x as flagValueToRow, y as flipFlagEnabled, h as mergeOverrides, E as persistConfigValue, D as persistFlagToggle, _ as registry, v as setFlagEnabled };
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { id as e, queryFor as t, registerRetention as n, retentionTtlMsFromEnv as r, table as i, text as a, timestamp as o } from "@voltro/database";
|
|
2
|
+
//#region src/usage.ts
|
|
3
|
+
var s = "_voltro_feature_flag_usage", c = i(s, {
|
|
4
|
+
id: e({ prefix: "flagusg" }),
|
|
5
|
+
flag: a(),
|
|
6
|
+
day: a(),
|
|
7
|
+
source: a(),
|
|
8
|
+
lastAt: o()
|
|
9
|
+
}).unique([
|
|
10
|
+
"flag",
|
|
11
|
+
"day",
|
|
12
|
+
"source"
|
|
13
|
+
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]), l = (e) => e.toISOString().slice(0, 10), u = (e) => {
|
|
14
|
+
let t = 2166136261;
|
|
15
|
+
for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
|
|
16
|
+
return (t >>> 0).toString(16).padStart(8, "0");
|
|
17
|
+
}, d = (e, t, n) => `flagusg_${t.replace(/-/g, "")}_${n === "targeted" ? "t" : "b"}_${u(e)}`, f = new class {
|
|
18
|
+
on = !1;
|
|
19
|
+
pending = /* @__PURE__ */ new Map();
|
|
20
|
+
liveCounts = /* @__PURE__ */ new Map();
|
|
21
|
+
enable() {
|
|
22
|
+
this.on = !0;
|
|
23
|
+
}
|
|
24
|
+
disable() {
|
|
25
|
+
this.on = !1, this.pending.clear();
|
|
26
|
+
}
|
|
27
|
+
get enabled() {
|
|
28
|
+
return this.on;
|
|
29
|
+
}
|
|
30
|
+
record(e, t, n = Date.now()) {
|
|
31
|
+
if (!this.on) return;
|
|
32
|
+
let r = `${t}\u0000${e}`, i = this.pending.get(r);
|
|
33
|
+
i ? i.lastAt = n : this.pending.set(r, {
|
|
34
|
+
flag: e,
|
|
35
|
+
source: t,
|
|
36
|
+
lastAt: n
|
|
37
|
+
}), t === "targeted" && this.liveCounts.set(e, (this.liveCounts.get(e) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
drain() {
|
|
40
|
+
let e = [...this.pending.values()];
|
|
41
|
+
return this.pending.clear(), e;
|
|
42
|
+
}
|
|
43
|
+
restore(e) {
|
|
44
|
+
if (this.on) for (let t of e) {
|
|
45
|
+
let e = `${t.source}\u0000${t.flag}`, n = this.pending.get(e);
|
|
46
|
+
(!n || n.lastAt < t.lastAt) && this.pending.set(e, { ...t });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
liveTargetedCounts() {
|
|
50
|
+
return this.liveCounts;
|
|
51
|
+
}
|
|
52
|
+
reset() {
|
|
53
|
+
this.on = !1, this.pending.clear(), this.liveCounts.clear();
|
|
54
|
+
}
|
|
55
|
+
}(), p = (e, t, n) => f.record(e, t, n), m = (e) => e === "bulk" ? "bulk" : "targeted", h = (e) => ({
|
|
56
|
+
upsertMany: async (t) => {
|
|
57
|
+
for (let n of t) {
|
|
58
|
+
let t = l(n.lastAt);
|
|
59
|
+
await e.upsert(s, {
|
|
60
|
+
id: d(n.flag, t, n.source),
|
|
61
|
+
flag: n.flag,
|
|
62
|
+
day: t,
|
|
63
|
+
source: n.source,
|
|
64
|
+
lastAt: n.lastAt
|
|
65
|
+
}, { conflictColumns: [
|
|
66
|
+
"flag",
|
|
67
|
+
"day",
|
|
68
|
+
"source"
|
|
69
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
load: async () => (await e.query(t(c).descriptor)).map((e) => ({
|
|
73
|
+
flag: String(e.flag),
|
|
74
|
+
day: String(e.day),
|
|
75
|
+
source: m(e.source),
|
|
76
|
+
lastAt: e.lastAt instanceof Date ? e.lastAt : new Date(String(e.lastAt))
|
|
77
|
+
}))
|
|
78
|
+
}), g = 3e5, _ = (e) => {
|
|
79
|
+
let t = e.intervalMs ?? 3e5, n = !1, r = async () => {
|
|
80
|
+
let t = f.drain();
|
|
81
|
+
if (t.length !== 0) try {
|
|
82
|
+
await e.store.upsertMany(t.map((e) => ({
|
|
83
|
+
flag: e.flag,
|
|
84
|
+
source: e.source,
|
|
85
|
+
lastAt: new Date(e.lastAt)
|
|
86
|
+
})));
|
|
87
|
+
} catch (n) {
|
|
88
|
+
f.restore(t), e.log?.warn?.("flag usage flush failed — observations retained for the next pass", {
|
|
89
|
+
pending: t.length,
|
|
90
|
+
cause: n.message
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, i = setInterval(() => {
|
|
94
|
+
r();
|
|
95
|
+
}, t);
|
|
96
|
+
return typeof i.unref == "function" && i.unref(), {
|
|
97
|
+
flushNow: r,
|
|
98
|
+
stop: async () => {
|
|
99
|
+
n || (n = !0, clearInterval(i), await r());
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, v = (e = process.env, t = 90) => {
|
|
103
|
+
let i = r(e.VOLTRO_FLAG_USAGE_TTL_HOURS, 24 * t);
|
|
104
|
+
return n({
|
|
105
|
+
table: s,
|
|
106
|
+
timeColumn: "lastAt",
|
|
107
|
+
source: "plugin",
|
|
108
|
+
ttlMs: i,
|
|
109
|
+
envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
|
|
110
|
+
}), i;
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
export { g as DEFAULT_FLAG_USAGE_FLUSH_MS, s as FLAG_USAGE_TABLE, h as dataStoreFlagUsageStore, f as flagUsageBuffer, d as flagUsageRowId, c as flagUsageTable, p as recordFlagEvaluation, v as registerFlagUsageRetention, _ as startFlagUsageFlush, l as utcDay };
|
package/dist/web.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/** `TypedFlag` with the value type erased — what the plugin and the registry
|
|
2
|
+
* hold. Sound because `TypedFlag` is covariant in `A` (see the annotation). */
|
|
3
|
+
declare type AnyTypedFlag = TypedFlag<unknown>;
|
|
4
|
+
|
|
5
|
+
declare interface ConfigEntry<out A> {
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly flag: TypedFlag<A>;
|
|
8
|
+
readonly public: boolean;
|
|
9
|
+
readonly restartRequired: 'room' | 'live';
|
|
10
|
+
readonly min: number | null;
|
|
11
|
+
readonly max: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare interface DefinedConfig<Values extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>> {
|
|
15
|
+
readonly entries: {
|
|
16
|
+
readonly [Key in keyof Values]: ConfigEntry<Values[Key]>;
|
|
17
|
+
};
|
|
18
|
+
readonly keys: readonly (keyof Values & string)[];
|
|
19
|
+
readonly typedFlags: readonly AnyTypedFlag[];
|
|
20
|
+
}
|
|
21
|
+
|
|
1
22
|
/** The result of decoding an UNTRUSTED value (a DB override, a dashboard edit)
|
|
2
23
|
* against a flag's Schema. Deliberately not an exception: the caller's correct
|
|
3
24
|
* response is to keep the code-declared value and say so, not to unwind. */
|
|
@@ -149,6 +170,9 @@ declare interface TypedFlagVariant<out A> {
|
|
|
149
170
|
readonly weight?: number;
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
/** Explicit Config catalog; only public keys are delivered by config.mine. */
|
|
174
|
+
export declare const useConfig: <Values extends Readonly<Record<string, unknown>>, Key extends keyof Values & string>(catalog: DefinedConfig<Values>, key: Key, apiName?: string) => Values[Key];
|
|
175
|
+
|
|
152
176
|
/** `true` iff a single flag is on for the caller. */
|
|
153
177
|
export declare const useFlag: (key: string, apiName?: string) => boolean;
|
|
154
178
|
|
package/dist/web.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { BASE_NAME as e } from "./rpc.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { typedValueOf as t } from "./define.js";
|
|
3
|
+
import { captureConfigValue as n } from "./config.js";
|
|
4
|
+
import { pluginTag as r } from "@voltro/protocol";
|
|
5
|
+
import { useSubscription as i } from "@voltro/client";
|
|
5
6
|
//#region src/web.ts
|
|
6
|
-
var
|
|
7
|
-
let { data: t } =
|
|
7
|
+
var a = (t) => r(e, t), o = (e = "app") => {
|
|
8
|
+
let { data: t } = i(e, a("evaluate"), {});
|
|
8
9
|
return t ?? {};
|
|
9
|
-
},
|
|
10
|
-
let { data: t } =
|
|
10
|
+
}, s = (e, t = "app") => o(t)[e] === !0, c = (e = "app") => {
|
|
11
|
+
let { data: t } = i(e, a("variants"), {});
|
|
11
12
|
return t ?? {};
|
|
12
|
-
},
|
|
13
|
+
}, l = (e, t = "app") => c(t)[e], u = (e, n = "app") => t(e, c(n)[e.key]), d = (e, t, r = "app") => {
|
|
14
|
+
let { data: a } = i(r, "config.mine", {}), o = e.entries[t];
|
|
15
|
+
if (!o.public) throw Error("useConfig requires an explicitly public Config key");
|
|
16
|
+
let s = o.flag.decode(a?.[t]);
|
|
17
|
+
return s.ok ? s.value : n(o.flag.defaultValue);
|
|
18
|
+
};
|
|
13
19
|
//#endregion
|
|
14
|
-
export {
|
|
20
|
+
export { d as useConfig, s as useFlag, u as useFlagValue, o as useFlags, l as useVariant, c as useVariants };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-flags",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.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",
|
|
@@ -38,7 +38,27 @@
|
|
|
38
38
|
"import": "./dist/rpc.js",
|
|
39
39
|
"default": "./dist/rpc.js"
|
|
40
40
|
},
|
|
41
|
-
"./package.json": "./package.json"
|
|
41
|
+
"./package.json": "./package.json",
|
|
42
|
+
"./config": {
|
|
43
|
+
"types": "./dist/config.d.ts",
|
|
44
|
+
"import": "./dist/config.js",
|
|
45
|
+
"default": "./dist/config.js"
|
|
46
|
+
},
|
|
47
|
+
"./config-port": {
|
|
48
|
+
"types": "./dist/config-port.d.ts",
|
|
49
|
+
"import": "./dist/config-port.js",
|
|
50
|
+
"default": "./dist/config-port.js"
|
|
51
|
+
},
|
|
52
|
+
"./config-inspect": {
|
|
53
|
+
"types": "./dist/config-inspect.d.ts",
|
|
54
|
+
"import": "./dist/config-inspect.js",
|
|
55
|
+
"default": "./dist/config-inspect.js"
|
|
56
|
+
},
|
|
57
|
+
"./config-testing": {
|
|
58
|
+
"types": "./dist/config-testing.d.ts",
|
|
59
|
+
"import": "./dist/config-testing.js",
|
|
60
|
+
"default": "./dist/config-testing.js"
|
|
61
|
+
}
|
|
42
62
|
},
|
|
43
63
|
"main": "./dist/index.js",
|
|
44
64
|
"module": "./dist/index.js",
|
|
@@ -48,10 +68,10 @@
|
|
|
48
68
|
"node": ">=24.0.0"
|
|
49
69
|
},
|
|
50
70
|
"dependencies": {
|
|
51
|
-
"@voltro/client": "0.
|
|
52
|
-
"@voltro/database": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/protocol": "0.
|
|
71
|
+
"@voltro/client": "0.87.0",
|
|
72
|
+
"@voltro/database": "0.87.0",
|
|
73
|
+
"@voltro/logger": "0.87.0",
|
|
74
|
+
"@voltro/protocol": "0.87.0"
|
|
55
75
|
},
|
|
56
76
|
"peerDependencies": {
|
|
57
77
|
"effect": "4.0.1"
|