@voltro/plugin-flags 0.83.1 → 0.85.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 +1191 -0
- package/THIRD-PARTY-NOTICES.md +1 -57
- package/dist/{define-D9Eet7wA.js → define-BHpLJh1F.js} +5 -5
- package/dist/errors.d.ts +4 -5
- package/dist/index.d.ts +6 -7
- package/dist/index.js +218 -215
- package/dist/rpc.d.ts +6 -6
- package/dist/rpc.js +2 -8
- package/dist/web.js +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { BASE_NAME as e, evaluateDescriptor as t, evaluateVariantsDescriptor as n, flagsRpcClientImports as r } from "./rpc.js";
|
|
2
|
-
import { i, n as a, r as o, t as s } from "./define-
|
|
2
|
+
import { i, n as a, r as o, t as s } from "./define-BHpLJh1F.js";
|
|
3
3
|
import { FlagDisabled as c } from "./errors.js";
|
|
4
|
-
import { Effect as l } from "effect";
|
|
5
|
-
import { definePlugin as
|
|
6
|
-
import { boolean as
|
|
4
|
+
import { Effect as l, Result as u } from "effect";
|
|
5
|
+
import { definePlugin as d, pluginInstanceName as f } from "@voltro/protocol";
|
|
6
|
+
import { boolean as p, derivedRowId as m, eq as h, id as g, integer as _, json as v, queryFor as y, registerRetention as b, retentionTtlMsFromEnv as x, table as S, text as C, timestamp as w } from "@voltro/database";
|
|
7
7
|
//#region src/evaluate.ts
|
|
8
|
-
var
|
|
8
|
+
var T = (e) => typeof e == "boolean" ? { enabled: e } : e, E = (e, t) => {
|
|
9
9
|
let n = 2166136261, r = `${e}:${t}`;
|
|
10
10
|
for (let e = 0; e < r.length; e++) n ^= r.charCodeAt(e), n = Math.imul(n, 16777619);
|
|
11
11
|
return n >>> 0;
|
|
12
|
-
},
|
|
12
|
+
}, D = (e, t) => E(e, t) % 100, O = (e, t) => E(`${e}:variant`, t) % 1e4, k = (e, t) => {
|
|
13
13
|
if (e.subjectIds && !(t.id != null && e.subjectIds.includes(t.id)) || e.tenantIds && !(t.tenantId != null && e.tenantIds.includes(t.tenantId)) || e.subjectTypes && !(t.type != null && e.subjectTypes.includes(t.type))) return !1;
|
|
14
14
|
if (e.metadata) {
|
|
15
15
|
let n = t.metadata ?? {};
|
|
16
16
|
for (let [t, r] of Object.entries(e.metadata)) if (String(n[t]) !== r) return !1;
|
|
17
17
|
}
|
|
18
18
|
return !0;
|
|
19
|
-
},
|
|
20
|
-
if (e.activateAt != null && t <
|
|
19
|
+
}, A = (e) => typeof e == "number" ? e : Date.parse(e), j = (e, t) => (e.rolloutBy === "tenant" ? t.tenantId : t.id) ?? "anon", M = (e, t) => {
|
|
20
|
+
if (e.activateAt != null && t < A(e.activateAt) || e.deactivateAt != null && t >= A(e.deactivateAt)) return { active: !1 };
|
|
21
21
|
if (e.ramp) {
|
|
22
|
-
let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o =
|
|
22
|
+
let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o = A(i), s = A(a);
|
|
23
23
|
if (t <= o) return {
|
|
24
24
|
active: !0,
|
|
25
25
|
rollout: n
|
|
@@ -35,71 +35,73 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
return { active: !0 };
|
|
38
|
-
},
|
|
39
|
-
let i =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
38
|
+
}, N = (e, t, n, r = Date.now()) => {
|
|
39
|
+
let i = T(t);
|
|
40
|
+
return P(e, i, n, r) ? !i.variants?.length || i.offVariant == null || I(e, i, n).name !== i.offVariant : !1;
|
|
41
|
+
}, P = (e, t, n, r) => {
|
|
42
|
+
if (t.enabled === !1) return !1;
|
|
43
|
+
let i;
|
|
44
|
+
if (t.schedule) {
|
|
45
|
+
let e = M(t.schedule, r);
|
|
44
46
|
if (!e.active) return !1;
|
|
45
|
-
|
|
47
|
+
i = e.rollout;
|
|
46
48
|
}
|
|
47
|
-
if (
|
|
48
|
-
let
|
|
49
|
-
if (
|
|
50
|
-
if (!(
|
|
51
|
-
} else if (
|
|
52
|
-
return
|
|
53
|
-
},
|
|
49
|
+
if (t.targeting && t.targeting.length > 0 && !t.targeting.some((e) => k(e, n))) return !1;
|
|
50
|
+
let a = i ?? t.rollout ?? 100;
|
|
51
|
+
if (a < 100 && a > 0) {
|
|
52
|
+
if (!(D(e, j(t, n)) < a)) return !1;
|
|
53
|
+
} else if (a <= 0) return !1;
|
|
54
|
+
return !0;
|
|
55
|
+
}, F = (e) => {
|
|
54
56
|
let t = e.reduce((e, t) => e + (t.weight ?? 1), 0) || e.length, n = 0;
|
|
55
57
|
return e.map((r, i) => (n += r.weight ?? 1, i === e.length - 1 ? 1e4 : Math.round(n / t * 1e4)));
|
|
56
|
-
},
|
|
58
|
+
}, I = (e, t, n) => {
|
|
57
59
|
let r = t.variants ?? [];
|
|
58
60
|
if (r.length === 0) throw Error(`flag "${e}" has no variants`);
|
|
59
|
-
let i =
|
|
61
|
+
let i = O(e, j(t, n)), a = F(r);
|
|
60
62
|
for (let e = 0; e < r.length; e++) if (i < a[e]) return r[e];
|
|
61
63
|
return r[r.length - 1];
|
|
62
|
-
},
|
|
63
|
-
let i =
|
|
64
|
+
}, L = (e, t, n, r = Date.now()) => {
|
|
65
|
+
let i = T(t), a = P(e, i, n, r);
|
|
64
66
|
if (!i.variants || i.variants.length === 0) return {
|
|
65
67
|
name: a ? "on" : "off",
|
|
66
68
|
value: a,
|
|
67
69
|
enabled: a
|
|
68
70
|
};
|
|
69
|
-
let o =
|
|
71
|
+
let o = I(e, i, n);
|
|
70
72
|
return {
|
|
71
73
|
name: o.name,
|
|
72
74
|
value: o.value,
|
|
73
|
-
enabled: a
|
|
75
|
+
enabled: a && o.name !== i.offVariant
|
|
74
76
|
};
|
|
75
|
-
},
|
|
77
|
+
}, ee = (e, t, n = Date.now()) => {
|
|
76
78
|
let r = {};
|
|
77
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
79
|
+
for (let [i, a] of Object.entries(e)) r[i] = N(i, a, t, n);
|
|
78
80
|
return r;
|
|
79
|
-
},
|
|
81
|
+
}, te = (e, t, n = Date.now()) => {
|
|
80
82
|
let r = {};
|
|
81
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
83
|
+
for (let [i, a] of Object.entries(e)) r[i] = L(i, a, t, n);
|
|
82
84
|
return r;
|
|
83
|
-
},
|
|
84
|
-
id:
|
|
85
|
-
key:
|
|
86
|
-
enabled:
|
|
87
|
-
rollout:
|
|
88
|
-
rolloutBy:
|
|
89
|
-
targeting:
|
|
90
|
-
variants:
|
|
91
|
-
offVariant:
|
|
92
|
-
schedule:
|
|
93
|
-
description:
|
|
94
|
-
updatedAt:
|
|
95
|
-
}),
|
|
96
|
-
id:
|
|
97
|
-
flag:
|
|
98
|
-
enabled:
|
|
99
|
-
previousEnabled:
|
|
100
|
-
actorId:
|
|
101
|
-
at:
|
|
102
|
-
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]),
|
|
85
|
+
}, R = "_voltro_feature_flag_audit", z = "_voltro_feature_flags", B = S(z, {
|
|
86
|
+
id: g({ prefix: "flag" }),
|
|
87
|
+
key: C().unique(),
|
|
88
|
+
enabled: p().default(!0),
|
|
89
|
+
rollout: _().nullable(),
|
|
90
|
+
rolloutBy: C().nullable(),
|
|
91
|
+
targeting: v().nullable(),
|
|
92
|
+
variants: v().nullable(),
|
|
93
|
+
offVariant: C().nullable(),
|
|
94
|
+
schedule: v().nullable(),
|
|
95
|
+
description: C().nullable(),
|
|
96
|
+
updatedAt: w().default("now").onUpdate("now")
|
|
97
|
+
}), V = S(R, {
|
|
98
|
+
id: g({ prefix: "flagaud" }),
|
|
99
|
+
flag: C(),
|
|
100
|
+
enabled: p(),
|
|
101
|
+
previousEnabled: p().nullable(),
|
|
102
|
+
actorId: C().nullable(),
|
|
103
|
+
at: w().default("now")
|
|
104
|
+
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), ne = (e, t) => {
|
|
103
105
|
let n = { ...e };
|
|
104
106
|
for (let e of t) {
|
|
105
107
|
let t = {
|
|
@@ -115,7 +117,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
115
117
|
n[e.key] = t;
|
|
116
118
|
}
|
|
117
119
|
return n;
|
|
118
|
-
},
|
|
120
|
+
}, H = new class {
|
|
119
121
|
flags = {};
|
|
120
122
|
set(e) {
|
|
121
123
|
this.flags = e;
|
|
@@ -126,19 +128,19 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
126
128
|
get(e) {
|
|
127
129
|
return this.flags[e];
|
|
128
130
|
}
|
|
129
|
-
}(),
|
|
130
|
-
let n =
|
|
131
|
+
}(), re = (e, t) => U(e, t) !== null, U = (e, t) => {
|
|
132
|
+
let n = H.get(e);
|
|
131
133
|
if (n === void 0) return null;
|
|
132
|
-
let r =
|
|
133
|
-
return
|
|
134
|
-
...
|
|
134
|
+
let r = T(n), i = r.enabled !== !1;
|
|
135
|
+
return H.set({
|
|
136
|
+
...H.all(),
|
|
135
137
|
[e]: {
|
|
136
138
|
...r,
|
|
137
139
|
enabled: t
|
|
138
140
|
}
|
|
139
141
|
}), i;
|
|
140
|
-
},
|
|
141
|
-
let n =
|
|
142
|
+
}, W = (e) => Object.entries(e).map(([e, t]) => {
|
|
143
|
+
let n = T(t);
|
|
142
144
|
return {
|
|
143
145
|
key: e,
|
|
144
146
|
enabled: n.enabled !== !1,
|
|
@@ -148,7 +150,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
148
150
|
description: n.description
|
|
149
151
|
};
|
|
150
152
|
}), ie = (e, t) => {
|
|
151
|
-
let n =
|
|
153
|
+
let n = T(t);
|
|
152
154
|
return {
|
|
153
155
|
key: e,
|
|
154
156
|
enabled: n.enabled !== !1,
|
|
@@ -161,7 +163,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
161
163
|
description: n.description ?? null
|
|
162
164
|
};
|
|
163
165
|
}, ae = (e) => ({
|
|
164
|
-
loadOverrides: async () => (await e.query(
|
|
166
|
+
loadOverrides: async () => (await e.query(y(B).descriptor)).map((e) => ({
|
|
165
167
|
key: String(e.key),
|
|
166
168
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
167
169
|
rollout: e.rollout == null ? null : Number(e.rollout),
|
|
@@ -173,11 +175,11 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
173
175
|
description: e.description ?? null
|
|
174
176
|
})),
|
|
175
177
|
upsertOverride: async (t) => {
|
|
176
|
-
let n =
|
|
177
|
-
await e.upsert(
|
|
178
|
+
let n = G(t);
|
|
179
|
+
await e.upsert(z, n, { conflictColumns: ["key"] });
|
|
178
180
|
}
|
|
179
|
-
}),
|
|
180
|
-
id:
|
|
181
|
+
}), G = (e) => ({
|
|
182
|
+
id: m("flag", e.key),
|
|
181
183
|
key: e.key,
|
|
182
184
|
enabled: e.enabled,
|
|
183
185
|
rollout: e.rollout,
|
|
@@ -196,7 +198,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
196
198
|
at: e.at instanceof Date ? e.at : new Date(String(e.at))
|
|
197
199
|
}), se = (e) => ({
|
|
198
200
|
record: async (t) => {
|
|
199
|
-
await e.insert(
|
|
201
|
+
await e.insert(R, {
|
|
200
202
|
id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
201
203
|
flag: t.flag,
|
|
202
204
|
enabled: t.enabled,
|
|
@@ -206,18 +208,18 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
206
208
|
});
|
|
207
209
|
},
|
|
208
210
|
list: async (t) => {
|
|
209
|
-
let n =
|
|
210
|
-
return t?.flag != null && (n = n.where(
|
|
211
|
+
let n = y(V).orderBy("at", "desc");
|
|
212
|
+
return t?.flag != null && (n = n.where(h("flag", t.flag))), t?.limit != null && (n = n.limit(t.limit)), (await e.query(n.descriptor)).map(oe);
|
|
211
213
|
}
|
|
212
214
|
}), ce = async (e, t, n, r, i) => {
|
|
213
215
|
await e.transactional(async (e) => {
|
|
214
|
-
await e.insertIgnore(
|
|
215
|
-
...
|
|
216
|
-
id:
|
|
216
|
+
await e.insertIgnore(z, {
|
|
217
|
+
...G(ie(t, n)),
|
|
218
|
+
id: m("flag", crypto.randomUUID())
|
|
217
219
|
}, { conflictColumns: ["key"] });
|
|
218
|
-
let a = (await e.query(
|
|
220
|
+
let a = (await e.query(y(B).where(h("key", t)).forUpdate().descriptor))[0];
|
|
219
221
|
if (!a) throw Error("flag override disappeared during toggle");
|
|
220
|
-
await e.update(
|
|
222
|
+
await e.update(z, String(a.id), {
|
|
221
223
|
enabled: r,
|
|
222
224
|
updatedAt: /* @__PURE__ */ new Date()
|
|
223
225
|
}), await se(e).record({
|
|
@@ -228,7 +230,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
228
230
|
at: /* @__PURE__ */ new Date()
|
|
229
231
|
});
|
|
230
232
|
});
|
|
231
|
-
},
|
|
233
|
+
}, le = (e, t) => {
|
|
232
234
|
let n = new Map(t.map((e) => [e.name, e])), r = [];
|
|
233
235
|
for (let t of e) {
|
|
234
236
|
let e = t.experiment;
|
|
@@ -267,24 +269,24 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
267
269
|
}
|
|
268
270
|
}
|
|
269
271
|
return r;
|
|
270
|
-
},
|
|
271
|
-
let n =
|
|
272
|
+
}, ue = (e, t) => {
|
|
273
|
+
let n = le(e, t);
|
|
272
274
|
if (n.length === 0) return;
|
|
273
275
|
let r = n.map((e) => ` · flag "${e.flag}" → experiment "${e.experiment}": ${e.problem}`);
|
|
274
276
|
throw Error(`@voltro/plugin-flags: ${n.length} flag→experiment link problem(s):\n${r.join("\n")}`);
|
|
275
|
-
},
|
|
276
|
-
let n =
|
|
277
|
+
}, de = 864e5, K = (e) => typeof e == "number" ? e : Date.parse(e), fe = (e, t) => {
|
|
278
|
+
let n = T(e);
|
|
277
279
|
if (n.enabled === !1) return {
|
|
278
280
|
shape: "constantOff",
|
|
279
281
|
reason: "enabled: false — resolves false for every caller"
|
|
280
282
|
};
|
|
281
283
|
if (n.schedule) {
|
|
282
284
|
let { activateAt: e, deactivateAt: r } = n.schedule;
|
|
283
|
-
if (r != null && t >=
|
|
285
|
+
if (r != null && t >= K(r)) return {
|
|
284
286
|
shape: "expired",
|
|
285
287
|
reason: `schedule.deactivateAt (${String(r)}) has passed — the flag can never be on again`
|
|
286
288
|
};
|
|
287
|
-
if (e != null && t <
|
|
289
|
+
if (e != null && t < K(e)) return {
|
|
288
290
|
shape: "notYetActive",
|
|
289
291
|
reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
|
|
290
292
|
};
|
|
@@ -294,7 +296,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
294
296
|
shape: "constantOff",
|
|
295
297
|
reason: `rollout: ${r} — nobody is in the rollout`
|
|
296
298
|
};
|
|
297
|
-
let i = (n.targeting?.length ?? 0) > 0, a = (n.variants?.length ?? 0) > 1, o = n.schedule !== void 0 && (n.schedule.ramp !== void 0 || n.schedule.deactivateAt != null && t <
|
|
299
|
+
let i = (n.targeting?.length ?? 0) > 0, a = (n.variants?.length ?? 0) > 1, o = n.schedule !== void 0 && (n.schedule.ramp !== void 0 || n.schedule.deactivateAt != null && t < K(n.schedule.deactivateAt));
|
|
298
300
|
return r >= 100 && !i && !a && !o ? {
|
|
299
301
|
shape: "constantOn",
|
|
300
302
|
reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
|
|
@@ -307,14 +309,14 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
307
309
|
o ? "a live schedule" : void 0
|
|
308
310
|
].filter((e) => e !== void 0).join(", ")})`
|
|
309
311
|
};
|
|
310
|
-
},
|
|
312
|
+
}, pe = [
|
|
311
313
|
"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.",
|
|
312
314
|
"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.",
|
|
313
315
|
"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.",
|
|
314
316
|
"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.",
|
|
315
317
|
"\"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."
|
|
316
|
-
],
|
|
317
|
-
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n *
|
|
318
|
+
], me = (e) => {
|
|
319
|
+
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n * de, i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o;
|
|
318
320
|
for (let t of e.usage) {
|
|
319
321
|
let e = t.lastAt.getTime();
|
|
320
322
|
if (!Number.isFinite(e)) continue;
|
|
@@ -322,7 +324,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
322
324
|
let n = t.source === "bulk" ? a : i, r = n.get(t.flag);
|
|
323
325
|
(r === void 0 || e > r) && n.set(t.flag, e);
|
|
324
326
|
}
|
|
325
|
-
let s = o === void 0 ? 0 : Math.floor((t - o) /
|
|
327
|
+
let s = o === void 0 ? 0 : Math.floor((t - o) / de);
|
|
326
328
|
return {
|
|
327
329
|
coverage: {
|
|
328
330
|
tracking: e.tracking,
|
|
@@ -332,7 +334,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
332
334
|
staleAfterDays: n
|
|
333
335
|
},
|
|
334
336
|
findings: Object.entries(e.flags).map(([o, c]) => {
|
|
335
|
-
let { shape: l, reason: u } =
|
|
337
|
+
let { shape: l, reason: u } = fe(c, t), d = i.get(o), f = a.get(o), p = [u], m;
|
|
336
338
|
e.tracking ? d !== void 0 && d >= r ? (m = "evaluated", p.push(`last server-side read ${new Date(d).toISOString()}`)) : d === void 0 ? (m = "neverObserved", p.push(s > 0 ? `no server-side read in the ${s}-day observation window` : "no server-side read observed yet (the observation window is empty)"), f !== void 0 && p.push("the flag WAS delivered to a client in a bulk flags.evaluate response — which does not prove it was read")) : (m = "stale", p.push(`last server-side read ${new Date(d).toISOString()}, more than ${n} day(s) ago, over an observation window of ${s} day(s)`)) : (m = "untracked", p.push("evaluation tracking is off — no usage evidence exists for this deployment"));
|
|
337
339
|
let h = l === "constantOn" || l === "constantOff" || l === "expired";
|
|
338
340
|
return h && p.push("the definition alone makes this flag a constant — it selects nothing"), {
|
|
@@ -347,23 +349,23 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
347
349
|
why: p
|
|
348
350
|
};
|
|
349
351
|
}),
|
|
350
|
-
limits:
|
|
352
|
+
limits: pe
|
|
351
353
|
};
|
|
352
|
-
}, q = "_voltro_feature_flag_usage", J =
|
|
353
|
-
id:
|
|
354
|
-
flag:
|
|
355
|
-
day:
|
|
356
|
-
source:
|
|
357
|
-
lastAt:
|
|
354
|
+
}, q = "_voltro_feature_flag_usage", J = S(q, {
|
|
355
|
+
id: g({ prefix: "flagusg" }),
|
|
356
|
+
flag: C(),
|
|
357
|
+
day: C(),
|
|
358
|
+
source: C(),
|
|
359
|
+
lastAt: w()
|
|
358
360
|
}).unique([
|
|
359
361
|
"flag",
|
|
360
362
|
"day",
|
|
361
363
|
"source"
|
|
362
|
-
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]),
|
|
364
|
+
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]), he = (e) => e.toISOString().slice(0, 10), ge = (e) => {
|
|
363
365
|
let t = 2166136261;
|
|
364
366
|
for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
|
|
365
367
|
return (t >>> 0).toString(16).padStart(8, "0");
|
|
366
|
-
},
|
|
368
|
+
}, _e = (e, t, n) => `flagusg_${t.replace(/-/g, "")}_${n === "targeted" ? "t" : "b"}_${ge(e)}`, Y = new class {
|
|
367
369
|
on = !1;
|
|
368
370
|
pending = /* @__PURE__ */ new Map();
|
|
369
371
|
liveCounts = /* @__PURE__ */ new Map();
|
|
@@ -401,12 +403,12 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
401
403
|
reset() {
|
|
402
404
|
this.on = !1, this.pending.clear(), this.liveCounts.clear();
|
|
403
405
|
}
|
|
404
|
-
}(), X = (e, t, n) => Y.record(e, t, n),
|
|
406
|
+
}(), X = (e, t, n) => Y.record(e, t, n), ve = (e) => e === "bulk" ? "bulk" : "targeted", ye = (e) => ({
|
|
405
407
|
upsertMany: async (t) => {
|
|
406
408
|
for (let n of t) {
|
|
407
|
-
let t =
|
|
409
|
+
let t = he(n.lastAt);
|
|
408
410
|
await e.upsert(q, {
|
|
409
|
-
id:
|
|
411
|
+
id: _e(n.flag, t, n.source),
|
|
410
412
|
flag: n.flag,
|
|
411
413
|
day: t,
|
|
412
414
|
source: n.source,
|
|
@@ -418,13 +420,13 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
418
420
|
] });
|
|
419
421
|
}
|
|
420
422
|
},
|
|
421
|
-
load: async () => (await e.query(
|
|
423
|
+
load: async () => (await e.query(y(J).descriptor)).map((e) => ({
|
|
422
424
|
flag: String(e.flag),
|
|
423
425
|
day: String(e.day),
|
|
424
|
-
source:
|
|
426
|
+
source: ve(e.source),
|
|
425
427
|
lastAt: e.lastAt instanceof Date ? e.lastAt : new Date(String(e.lastAt))
|
|
426
428
|
}))
|
|
427
|
-
}),
|
|
429
|
+
}), be = 3e5, xe = (e) => {
|
|
428
430
|
let t = e.intervalMs ?? 3e5, n = !1, r = async () => {
|
|
429
431
|
let t = Y.drain();
|
|
430
432
|
if (t.length !== 0) try {
|
|
@@ -448,9 +450,9 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
448
450
|
n || (n = !0, clearInterval(i), await r());
|
|
449
451
|
}
|
|
450
452
|
};
|
|
451
|
-
},
|
|
452
|
-
let n =
|
|
453
|
-
return
|
|
453
|
+
}, Se = (e = process.env, t = 90) => {
|
|
454
|
+
let n = x(e.VOLTRO_FLAG_USAGE_TTL_HOURS, 24 * t);
|
|
455
|
+
return b({
|
|
454
456
|
table: q,
|
|
455
457
|
timeColumn: "lastAt",
|
|
456
458
|
source: "plugin",
|
|
@@ -458,127 +460,127 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
458
460
|
envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
|
|
459
461
|
}), n;
|
|
460
462
|
}, Z = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, Q = (e, t) => {
|
|
461
|
-
let n =
|
|
462
|
-
return X(t, "targeted"), n !== void 0 &&
|
|
463
|
-
},
|
|
464
|
-
let n =
|
|
465
|
-
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : i(t,
|
|
466
|
-
},
|
|
467
|
-
let n =
|
|
468
|
-
return X(t.key, "targeted"), n === void 0 ? null :
|
|
469
|
-
},
|
|
470
|
-
let i = e.gatedBy ?? {}, o =
|
|
463
|
+
let n = H.get(t);
|
|
464
|
+
return X(t, "targeted"), n !== void 0 && N(t, n, Z(e));
|
|
465
|
+
}, Ce = (e, t) => Q(e, t) ? l.void : l.fail(new c({ flag: t })), we = (e, t) => {
|
|
466
|
+
let n = H.get(t.key);
|
|
467
|
+
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : i(t, L(t.key, n, Z(e)));
|
|
468
|
+
}, Te = (e, t) => {
|
|
469
|
+
let n = H.get(t.key);
|
|
470
|
+
return X(t.key, "targeted"), n === void 0 ? null : L(t.key, n, Z(e)).name;
|
|
471
|
+
}, Ee = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), $ = e, De = (e = {}) => {
|
|
472
|
+
let i = e.gatedBy ?? {}, o = f({
|
|
471
473
|
base: $,
|
|
472
474
|
alias: e.alias,
|
|
473
475
|
instance: e.name
|
|
474
|
-
}), s = e.typedFlags ?? [],
|
|
476
|
+
}), s = e.typedFlags ?? [], p = /* @__PURE__ */ new Map();
|
|
475
477
|
for (let t of s) {
|
|
476
|
-
if (
|
|
478
|
+
if (p.has(t.key)) throw Error(`${$}: two typedFlags declare the key ${JSON.stringify(t.key)}`);
|
|
477
479
|
if (e.flags?.[t.key] !== void 0) throw Error(`${$}: ${JSON.stringify(t.key)} is declared BOTH in \`flags\` and in \`typedFlags\`. One key, one declaration — the typed one would otherwise be silently overwritten by the untyped baseline.`);
|
|
478
|
-
|
|
480
|
+
p.set(t.key, t);
|
|
479
481
|
}
|
|
480
|
-
s.some((e) => e.experiment !== void 0) &&
|
|
481
|
-
let
|
|
482
|
-
for (let e of s)
|
|
483
|
-
let
|
|
484
|
-
if (
|
|
485
|
-
let
|
|
486
|
-
if (
|
|
487
|
-
let
|
|
488
|
-
let t =
|
|
489
|
-
for (let [e, r] of
|
|
482
|
+
s.some((e) => e.experiment !== void 0) && ue(s, e.experiments ?? []);
|
|
483
|
+
let m = { ...e.flags ?? {} };
|
|
484
|
+
for (let e of s) m[e.key] = e.definition;
|
|
485
|
+
let h = e.store === "postgres", g, _, v, y, b, x, S, C = [], w = e.usage ?? {}, T = w.track ?? h;
|
|
486
|
+
if (T && !h) throw Error(`${$}: usage.track requires store: 'postgres' — there is nowhere to record an evaluation on the memory tier, and a dead-flag report that silently observed nothing is exactly the defect it exists to find.`);
|
|
487
|
+
let E = w.staleAfterDays ?? 30, D = w.retentionDays ?? 90;
|
|
488
|
+
if (E > D) throw Error(`${$}: usage.staleAfterDays (${E}) exceeds usage.retentionDays (${D}), so the observation window can never be long enough to prove a flag stale and every verdict would be withheld.`);
|
|
489
|
+
let O = (e) => {
|
|
490
|
+
let t = ne(m, e), n = [];
|
|
491
|
+
for (let [e, r] of p) {
|
|
490
492
|
let i = t[e];
|
|
491
493
|
if (i === void 0) continue;
|
|
492
494
|
let o = a(r, typeof i == "boolean" ? { enabled: i } : i);
|
|
493
495
|
o.ok || (n.push(o.refusal), t[e] = r.definition);
|
|
494
496
|
}
|
|
495
|
-
|
|
496
|
-
},
|
|
497
|
-
if (!
|
|
498
|
-
let e = ++
|
|
499
|
-
e ===
|
|
500
|
-
},
|
|
501
|
-
if (
|
|
502
|
-
if (await
|
|
503
|
-
|
|
504
|
-
let e =
|
|
505
|
-
|
|
506
|
-
store:
|
|
507
|
-
intervalMs:
|
|
497
|
+
C = n, H.set(t);
|
|
498
|
+
}, k = 0, A = async () => {
|
|
499
|
+
if (!_) return;
|
|
500
|
+
let e = ++k, t = await _.loadOverrides();
|
|
501
|
+
e === k && O(t);
|
|
502
|
+
}, j = async () => {
|
|
503
|
+
if (x && _) {
|
|
504
|
+
if (await A(), T && y && !b) {
|
|
505
|
+
S = Se(process.env, D), Y.enable();
|
|
506
|
+
let e = x.logger;
|
|
507
|
+
b = xe({
|
|
508
|
+
store: y,
|
|
509
|
+
intervalMs: w.flushIntervalMs ?? 3e5,
|
|
508
510
|
log: { warn: (t, n) => e.warn(t, n) }
|
|
509
511
|
});
|
|
510
512
|
}
|
|
511
|
-
for (let e of
|
|
513
|
+
for (let e of C) x.logger.warn("feature flag override REFUSED — code-declared definition stands", {
|
|
512
514
|
flag: e.key,
|
|
513
515
|
reason: e.reason
|
|
514
516
|
});
|
|
515
517
|
}
|
|
516
|
-
},
|
|
518
|
+
}, M = (e) => {
|
|
517
519
|
if (i[e] !== void 0) return i[e];
|
|
518
|
-
for (let [t, n] of Object.entries(i)) if (t !== e &&
|
|
519
|
-
},
|
|
520
|
-
let n =
|
|
520
|
+
for (let [t, n] of Object.entries(i)) if (t !== e && Ee(t, e)) return n;
|
|
521
|
+
}, N = (e, t) => {
|
|
522
|
+
let n = M(t.tag);
|
|
521
523
|
return n === void 0 || Q({ subject: t.subject }, n) ? e : l.fail(new c({ flag: n }));
|
|
522
|
-
},
|
|
524
|
+
}, P = Object.keys(i).length > 0, F = (e) => {
|
|
523
525
|
for (let t of e) X(t, "bulk");
|
|
524
|
-
},
|
|
526
|
+
}, I = {
|
|
525
527
|
...t,
|
|
526
528
|
description: "Resolve every feature flag for the calling subject.",
|
|
527
529
|
execute: (e, t) => l.sync(() => {
|
|
528
|
-
let e =
|
|
529
|
-
return
|
|
530
|
+
let e = H.all();
|
|
531
|
+
return F(Object.keys(e)), ee(e, t.request.subject ?? {});
|
|
530
532
|
})
|
|
531
|
-
},
|
|
533
|
+
}, L = {
|
|
532
534
|
...n,
|
|
533
535
|
description: "Resolve every feature flag to its served variant for the calling subject.",
|
|
534
536
|
execute: (e, t) => l.sync(() => {
|
|
535
|
-
let e =
|
|
536
|
-
return
|
|
537
|
+
let e = H.all();
|
|
538
|
+
return F(Object.keys(e)), te(e, t.request.subject ?? {});
|
|
537
539
|
})
|
|
538
|
-
},
|
|
540
|
+
}, R = (e) => ({
|
|
539
541
|
kind: "json",
|
|
540
542
|
data: e
|
|
541
|
-
}),
|
|
543
|
+
}), z = (e, t) => ({
|
|
542
544
|
kind: "json",
|
|
543
545
|
status: e,
|
|
544
546
|
data: { error: t }
|
|
545
|
-
}),
|
|
547
|
+
}), re = async () => {
|
|
546
548
|
let e = [];
|
|
547
|
-
if (
|
|
548
|
-
e = await
|
|
549
|
+
if (T && y) try {
|
|
550
|
+
e = await y.load();
|
|
549
551
|
} catch {
|
|
550
552
|
e = [];
|
|
551
553
|
}
|
|
552
|
-
return
|
|
553
|
-
flags:
|
|
554
|
+
return me({
|
|
555
|
+
flags: H.all(),
|
|
554
556
|
usage: e,
|
|
555
|
-
tracking:
|
|
557
|
+
tracking: T,
|
|
556
558
|
liveCounts: Y.liveTargetedCounts(),
|
|
557
|
-
staleAfterDays:
|
|
558
|
-
retentionDays:
|
|
559
|
+
staleAfterDays: E,
|
|
560
|
+
retentionDays: D
|
|
559
561
|
});
|
|
560
|
-
},
|
|
562
|
+
}, ie = () => {
|
|
561
563
|
let e = {};
|
|
562
|
-
for (let [t, n] of
|
|
564
|
+
for (let [t, n] of p) e[t] = {
|
|
563
565
|
valueType: n.valueType,
|
|
564
566
|
variants: n.variants.map((e) => e.name),
|
|
565
567
|
experiment: n.experiment ?? null
|
|
566
568
|
};
|
|
567
569
|
return e;
|
|
568
|
-
},
|
|
570
|
+
}, G = [
|
|
569
571
|
{
|
|
570
572
|
method: "GET",
|
|
571
573
|
path: "/list",
|
|
572
574
|
scope: "process",
|
|
573
575
|
description: "Every resolved flag (key, enabled, rollout, description) plus the dead-flag lifecycle report: per-flag shape + usage verdict, the observation coverage window, and the limits of what the report can prove.",
|
|
574
576
|
handler: () => l.promise(async () => {
|
|
575
|
-
let e = await
|
|
576
|
-
return
|
|
577
|
-
flags: H
|
|
578
|
-
writeScope:
|
|
577
|
+
let e = await re();
|
|
578
|
+
return R({
|
|
579
|
+
flags: W(H.all()),
|
|
580
|
+
writeScope: h ? "shared-store" : "process",
|
|
579
581
|
lifecycle: e,
|
|
580
|
-
typed:
|
|
581
|
-
overrideRefusals:
|
|
582
|
+
typed: ie(),
|
|
583
|
+
overrideRefusals: C
|
|
582
584
|
});
|
|
583
585
|
})
|
|
584
586
|
},
|
|
@@ -591,39 +593,40 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
591
593
|
try {
|
|
592
594
|
t = JSON.parse(e.body || "{}");
|
|
593
595
|
} catch {
|
|
594
|
-
return
|
|
596
|
+
return z(400, "invalid JSON body");
|
|
595
597
|
}
|
|
596
|
-
if (!t || typeof t.key != "string" || !t.key || typeof t.enabled != "boolean") return
|
|
597
|
-
let n = t.key, r = t.enabled, i =
|
|
598
|
-
if (i === void 0) return
|
|
599
|
-
if (
|
|
600
|
-
let e =
|
|
601
|
-
if (!e) return
|
|
602
|
-
|
|
603
|
-
await ce(e, n, i, r, typeof t.actor == "string" ? t.actor : null), await
|
|
604
|
-
}).pipe(l.
|
|
605
|
-
|
|
606
|
-
|
|
598
|
+
if (!t || typeof t.key != "string" || !t.key || typeof t.enabled != "boolean") return z(400, "key (string) + enabled (boolean) required");
|
|
599
|
+
let n = t.key, r = t.enabled, i = H.get(n);
|
|
600
|
+
if (i === void 0) return z(404, `unknown flag "${n}"`);
|
|
601
|
+
if (h) {
|
|
602
|
+
let e = g;
|
|
603
|
+
if (!e) return z(503, "flag store is not ready");
|
|
604
|
+
let a = yield* l.tryPromise(async () => {
|
|
605
|
+
await ce(e, n, i, r, typeof t.actor == "string" ? t.actor : null), await A();
|
|
606
|
+
}).pipe(l.result);
|
|
607
|
+
if (u.isFailure(a)) return yield* l.tryPromise(A).pipe(l.ignore), z(503, "flag update could not be confirmed; reload the flag state before retrying");
|
|
608
|
+
} else U(n, r);
|
|
609
|
+
return R({
|
|
607
610
|
ok: !0,
|
|
608
|
-
flags: H
|
|
611
|
+
flags: W(H.all())
|
|
609
612
|
});
|
|
610
613
|
})
|
|
611
614
|
},
|
|
612
615
|
{
|
|
613
616
|
method: "GET",
|
|
614
617
|
path: "/audit",
|
|
615
|
-
scope:
|
|
618
|
+
scope: h ? "shared-store" : "declaration",
|
|
616
619
|
description: "The kill-switch flip audit trail, newest-first. `?flag=<key>` filters; `?limit=<n>` caps (postgres tier only).",
|
|
617
620
|
handler: (e) => l.gen(function* () {
|
|
618
|
-
if (!
|
|
621
|
+
if (!v) return R({
|
|
619
622
|
audit: [],
|
|
620
623
|
note: "audit trail requires store: \"postgres\""
|
|
621
624
|
});
|
|
622
|
-
let t = new URL(e.url, "http://local"), n = t.searchParams.get("flag"), r = t.searchParams.get("limit"), i = r != null && Number.isFinite(Number(r)) ? Number(r) : 100, a = yield* l.promise(() =>
|
|
625
|
+
let t = new URL(e.url, "http://local"), n = t.searchParams.get("flag"), r = t.searchParams.get("limit"), i = r != null && Number.isFinite(Number(r)) ? Number(r) : 100, a = yield* l.promise(() => v.list({
|
|
623
626
|
...n == null ? {} : { flag: n },
|
|
624
627
|
limit: i
|
|
625
628
|
}));
|
|
626
|
-
return
|
|
629
|
+
return R({ audit: a.map((e) => ({
|
|
627
630
|
flag: e.flag,
|
|
628
631
|
enabled: e.enabled,
|
|
629
632
|
previousEnabled: e.previousEnabled,
|
|
@@ -632,25 +635,25 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
632
635
|
})) });
|
|
633
636
|
})
|
|
634
637
|
}
|
|
635
|
-
],
|
|
636
|
-
...
|
|
638
|
+
], oe = [
|
|
639
|
+
...P ? [
|
|
637
640
|
"rpc:intercept:mutation",
|
|
638
641
|
"rpc:intercept:query",
|
|
639
642
|
"rpc:intercept:action",
|
|
640
643
|
"rpc:intercept:workflow"
|
|
641
644
|
] : [],
|
|
642
|
-
...
|
|
645
|
+
...h ? ["store:write", "store:changes:read"] : [],
|
|
643
646
|
"inspect:read",
|
|
644
647
|
"inspect:write"
|
|
645
648
|
];
|
|
646
|
-
return
|
|
649
|
+
return d({
|
|
647
650
|
name: o,
|
|
648
651
|
baseName: $,
|
|
649
652
|
description: "Feature flags — targeting, % rollout, kill-switch, declarative gating + client evaluation.",
|
|
650
|
-
permissions:
|
|
651
|
-
routes: [
|
|
653
|
+
permissions: oe,
|
|
654
|
+
routes: [I, L],
|
|
652
655
|
rpcClientDescriptors: r,
|
|
653
|
-
inspectEndpoints:
|
|
656
|
+
inspectEndpoints: G,
|
|
654
657
|
errorSchemas: [{
|
|
655
658
|
schema: c,
|
|
656
659
|
import: {
|
|
@@ -658,34 +661,34 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
658
661
|
name: "FlagDisabled"
|
|
659
662
|
}
|
|
660
663
|
}],
|
|
661
|
-
...
|
|
662
|
-
interceptMutation:
|
|
663
|
-
interceptQuery:
|
|
664
|
-
interceptAction:
|
|
665
|
-
interceptWorkflow:
|
|
664
|
+
...P ? {
|
|
665
|
+
interceptMutation: N,
|
|
666
|
+
interceptQuery: N,
|
|
667
|
+
interceptAction: N,
|
|
668
|
+
interceptWorkflow: N
|
|
666
669
|
} : {},
|
|
667
|
-
...
|
|
668
|
-
|
|
669
|
-
|
|
670
|
+
...h ? { extendSchema: { tables: T ? [
|
|
671
|
+
B,
|
|
672
|
+
V,
|
|
670
673
|
J
|
|
671
|
-
] : [
|
|
672
|
-
...
|
|
673
|
-
|
|
674
|
+
] : [B, V] } } : {},
|
|
675
|
+
...h ? { bindDataStore: async (e) => {
|
|
676
|
+
g = e, _ = ae(g), v = se(e), T && (y = ye(e)), await j();
|
|
674
677
|
} } : {},
|
|
675
|
-
...
|
|
678
|
+
...h ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? l.tryPromise(() => A()) : l.void } : {},
|
|
676
679
|
onActivate: (t) => l.gen(function* () {
|
|
677
|
-
|
|
678
|
-
count: Object.keys(
|
|
679
|
-
typed:
|
|
680
|
+
H.set(m), x = t, _ && (yield* l.promise(j)), t.logger.info("feature flags active", {
|
|
681
|
+
count: Object.keys(m).length,
|
|
682
|
+
typed: p.size,
|
|
680
683
|
gated: Object.keys(i).length,
|
|
681
684
|
store: e.store ?? "memory",
|
|
682
|
-
usage:
|
|
685
|
+
usage: T ? b ? `tracking (stale after ${E}d, observations kept ${Math.round((S ?? 0) / 864e5)}d)` : "waiting for store binding" : "off — no dead-flag evidence is collected"
|
|
683
686
|
});
|
|
684
687
|
}),
|
|
685
688
|
onDeactivate: () => l.promise(async () => {
|
|
686
|
-
|
|
689
|
+
x = void 0, await b?.stop(), b = void 0, Y.disable(), H.set({});
|
|
687
690
|
})
|
|
688
691
|
});
|
|
689
692
|
};
|
|
690
693
|
//#endregion
|
|
691
|
-
export {
|
|
694
|
+
export { be as DEFAULT_FLAG_USAGE_FLUSH_MS, pe as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, c as FlagDisabled, ue as assertFlagExperimentLinks, le as checkFlagExperimentLinks, fe as classifyShape, ye as dataStoreFlagUsageStore, s as defineFlag, W as describeFlags, ee as evaluateAll, te as evaluateAllVariants, N as evaluateFlag, M as evaluateSchedule, L as evaluateVariant, B as featureFlagsTable, V as flagAuditTable, me as flagLifecycleReport, H as flagRegistry, Y as flagUsageBuffer, _e as flagUsageRowId, J as flagUsageTable, we as flagValue, Te as flagVariant, De as flagsPlugin, U as flipFlagEnabled, a as gateOverride, Q as isFlagEnabled, o as isTypedFlag, ne as mergeOverrides, X as recordFlagEvaluation, Se as registerFlagUsageRetention, Ce as requireFlag, I as resolveVariant, re as setFlagEnabled, xe as startFlagUsageFlush, T as toDefinition, i as typedValueOf, he as utcDay };
|