@voltro/plugin-flags 0.54.0 → 0.56.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 +639 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +188 -188
- package/dist/rpc.d.ts +12 -0
- package/dist/rpc.js +6 -6
- package/dist/web.js +12 -10
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i
|
|
3
|
-
import { FlagDisabled as
|
|
4
|
-
import { Effect as
|
|
5
|
-
import { definePlugin as
|
|
6
|
-
import { boolean as
|
|
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-D9Eet7wA.js";
|
|
3
|
+
import { FlagDisabled as c } from "./errors.js";
|
|
4
|
+
import { Effect as l } from "effect";
|
|
5
|
+
import { definePlugin as u, pluginInstanceName as d } from "@voltro/protocol";
|
|
6
|
+
import { boolean as f, derivedRowId as p, id as m, integer as h, json as g, queryFor as _, registerRetention as v, retentionTtlMsFromEnv as y, table as b, text as x, timestamp as S } from "@voltro/database";
|
|
7
7
|
//#region src/evaluate.ts
|
|
8
|
-
var
|
|
8
|
+
var C = (e) => typeof e == "boolean" ? { enabled: e } : e, w = (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
|
+
}, T = (e, t) => w(e, t) % 100, ee = (e, t) => w(`${e}:variant`, t) % 1e4, E = (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
|
+
}, D = (e) => typeof e == "number" ? e : Date.parse(e), O = (e, t) => (e.rolloutBy === "tenant" ? t.tenantId : t.id) ?? "anon", k = (e, t) => {
|
|
20
|
+
if (e.activateAt != null && t < D(e.activateAt) || e.deactivateAt != null && t >= D(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 = D(i), s = D(a);
|
|
23
23
|
if (t <= o) return {
|
|
24
24
|
active: !0,
|
|
25
25
|
rollout: n
|
|
@@ -35,71 +35,71 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
return { active: !0 };
|
|
38
|
-
},
|
|
39
|
-
let i =
|
|
38
|
+
}, A = (e, t, n, r = Date.now()) => {
|
|
39
|
+
let i = C(t);
|
|
40
40
|
if (i.enabled === !1) return !1;
|
|
41
41
|
let a;
|
|
42
42
|
if (i.schedule) {
|
|
43
|
-
let e =
|
|
43
|
+
let e = k(i.schedule, r);
|
|
44
44
|
if (!e.active) return !1;
|
|
45
45
|
a = e.rollout;
|
|
46
46
|
}
|
|
47
|
-
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) =>
|
|
47
|
+
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) => E(e, n))) return !1;
|
|
48
48
|
let o = a ?? i.rollout ?? 100;
|
|
49
49
|
if (o < 100 && o > 0) {
|
|
50
|
-
if (!(
|
|
50
|
+
if (!(T(e, O(i, n)) < o)) return !1;
|
|
51
51
|
} else if (o <= 0) return !1;
|
|
52
|
-
return i.variants && i.variants.length > 0 && i.offVariant != null ?
|
|
53
|
-
},
|
|
52
|
+
return i.variants && i.variants.length > 0 && i.offVariant != null ? M(e, i, n).name !== i.offVariant : !0;
|
|
53
|
+
}, j = (e) => {
|
|
54
54
|
let t = e.reduce((e, t) => e + (t.weight ?? 1), 0) || e.length, n = 0;
|
|
55
55
|
return e.map((r, i) => (n += r.weight ?? 1, i === e.length - 1 ? 1e4 : Math.round(n / t * 1e4)));
|
|
56
|
-
},
|
|
56
|
+
}, M = (e, t, n) => {
|
|
57
57
|
let r = t.variants ?? [];
|
|
58
58
|
if (r.length === 0) throw Error(`flag "${e}" has no variants`);
|
|
59
|
-
let i =
|
|
59
|
+
let i = ee(e, O(t, n)), a = j(r);
|
|
60
60
|
for (let e = 0; e < r.length; e++) if (i < a[e]) return r[e];
|
|
61
61
|
return r[r.length - 1];
|
|
62
|
-
},
|
|
63
|
-
let i =
|
|
62
|
+
}, N = (e, t, n, r = Date.now()) => {
|
|
63
|
+
let i = C(t), a = A(e, t, n, r);
|
|
64
64
|
if (!i.variants || i.variants.length === 0) return {
|
|
65
65
|
name: a ? "on" : "off",
|
|
66
66
|
value: a,
|
|
67
67
|
enabled: a
|
|
68
68
|
};
|
|
69
|
-
let o =
|
|
69
|
+
let o = M(e, i, n);
|
|
70
70
|
return {
|
|
71
71
|
name: o.name,
|
|
72
72
|
value: o.value,
|
|
73
73
|
enabled: a
|
|
74
74
|
};
|
|
75
|
-
},
|
|
75
|
+
}, te = (e, t, n = Date.now()) => {
|
|
76
76
|
let r = {};
|
|
77
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
77
|
+
for (let [i, a] of Object.entries(e)) r[i] = A(i, a, t, n);
|
|
78
78
|
return r;
|
|
79
|
-
},
|
|
79
|
+
}, ne = (e, t, n = Date.now()) => {
|
|
80
80
|
let r = {};
|
|
81
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
81
|
+
for (let [i, a] of Object.entries(e)) r[i] = N(i, a, t, n);
|
|
82
82
|
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"]),
|
|
83
|
+
}, P = "_voltro_feature_flag_audit", F = "_voltro_feature_flags", I = b(F, {
|
|
84
|
+
id: m({ prefix: "flag" }),
|
|
85
|
+
key: x().unique(),
|
|
86
|
+
enabled: f().default(!0),
|
|
87
|
+
rollout: h().nullable(),
|
|
88
|
+
rolloutBy: x().nullable(),
|
|
89
|
+
targeting: g().nullable(),
|
|
90
|
+
variants: g().nullable(),
|
|
91
|
+
offVariant: x().nullable(),
|
|
92
|
+
schedule: g().nullable(),
|
|
93
|
+
description: x().nullable(),
|
|
94
|
+
updatedAt: S().default("now").onUpdate("now")
|
|
95
|
+
}), L = b(P, {
|
|
96
|
+
id: m({ prefix: "flagaud" }),
|
|
97
|
+
flag: x(),
|
|
98
|
+
enabled: f(),
|
|
99
|
+
previousEnabled: f().nullable(),
|
|
100
|
+
actorId: x().nullable(),
|
|
101
|
+
at: S().default("now")
|
|
102
|
+
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), re = (e, t) => {
|
|
103
103
|
let n = { ...e };
|
|
104
104
|
for (let e of t) {
|
|
105
105
|
let t = {
|
|
@@ -115,7 +115,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
115
115
|
n[e.key] = t;
|
|
116
116
|
}
|
|
117
117
|
return n;
|
|
118
|
-
},
|
|
118
|
+
}, R = new class {
|
|
119
119
|
flags = {};
|
|
120
120
|
set(e) {
|
|
121
121
|
this.flags = e;
|
|
@@ -126,19 +126,19 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
126
126
|
get(e) {
|
|
127
127
|
return this.flags[e];
|
|
128
128
|
}
|
|
129
|
-
}(),
|
|
130
|
-
let n =
|
|
129
|
+
}(), z = (e, t) => B(e, t) !== null, B = (e, t) => {
|
|
130
|
+
let n = R.get(e);
|
|
131
131
|
if (n === void 0) return null;
|
|
132
|
-
let r =
|
|
133
|
-
return
|
|
134
|
-
...
|
|
132
|
+
let r = C(n), i = r.enabled !== !1;
|
|
133
|
+
return R.set({
|
|
134
|
+
...R.all(),
|
|
135
135
|
[e]: {
|
|
136
136
|
...r,
|
|
137
137
|
enabled: t
|
|
138
138
|
}
|
|
139
139
|
}), i;
|
|
140
|
-
},
|
|
141
|
-
let n =
|
|
140
|
+
}, V = (e) => Object.entries(e).map(([e, t]) => {
|
|
141
|
+
let n = C(t);
|
|
142
142
|
return {
|
|
143
143
|
key: e,
|
|
144
144
|
enabled: n.enabled !== !1,
|
|
@@ -148,7 +148,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
148
148
|
description: n.description
|
|
149
149
|
};
|
|
150
150
|
}), ie = (e, t) => {
|
|
151
|
-
let n =
|
|
151
|
+
let n = C(t);
|
|
152
152
|
return {
|
|
153
153
|
key: e,
|
|
154
154
|
enabled: n.enabled !== !1,
|
|
@@ -161,7 +161,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
161
161
|
description: n.description ?? null
|
|
162
162
|
};
|
|
163
163
|
}, ae = (e) => ({
|
|
164
|
-
loadOverrides: async () => (await e.query(
|
|
164
|
+
loadOverrides: async () => (await e.query(_(I).descriptor)).map((e) => ({
|
|
165
165
|
key: String(e.key),
|
|
166
166
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
167
167
|
rollout: e.rollout == null ? null : Number(e.rollout),
|
|
@@ -174,7 +174,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
174
174
|
})),
|
|
175
175
|
upsertOverride: async (t) => {
|
|
176
176
|
let n = {
|
|
177
|
-
id:
|
|
177
|
+
id: p("flag", t.key),
|
|
178
178
|
key: t.key,
|
|
179
179
|
enabled: t.enabled,
|
|
180
180
|
rollout: t.rollout,
|
|
@@ -186,7 +186,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
186
186
|
description: t.description,
|
|
187
187
|
updatedAt: /* @__PURE__ */ new Date()
|
|
188
188
|
};
|
|
189
|
-
await e.upsert(
|
|
189
|
+
await e.upsert(F, n, { conflictColumns: ["key"] });
|
|
190
190
|
}
|
|
191
191
|
}), oe = (e) => ({
|
|
192
192
|
flag: String(e.flag),
|
|
@@ -196,7 +196,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
196
196
|
at: e.at instanceof Date ? e.at : new Date(String(e.at))
|
|
197
197
|
}), se = (e) => ({
|
|
198
198
|
record: async (t) => {
|
|
199
|
-
await e.insert(
|
|
199
|
+
await e.insert(P, {
|
|
200
200
|
id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
201
201
|
flag: t.flag,
|
|
202
202
|
enabled: t.enabled,
|
|
@@ -206,12 +206,12 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
206
206
|
});
|
|
207
207
|
},
|
|
208
208
|
list: async (t) => {
|
|
209
|
-
let n =
|
|
209
|
+
let n = _(L).orderBy("at", "desc");
|
|
210
210
|
t?.limit != null && (n = n.limit(t.limit));
|
|
211
211
|
let r = (await e.query(n.descriptor)).map(oe);
|
|
212
212
|
return t?.flag == null ? r : r.filter((e) => e.flag === t.flag);
|
|
213
213
|
}
|
|
214
|
-
}),
|
|
214
|
+
}), H = (e, t) => {
|
|
215
215
|
let n = new Map(t.map((e) => [e.name, e])), r = [];
|
|
216
216
|
for (let t of e) {
|
|
217
217
|
let e = t.experiment;
|
|
@@ -250,24 +250,24 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
return r;
|
|
253
|
-
},
|
|
254
|
-
let n =
|
|
253
|
+
}, U = (e, t) => {
|
|
254
|
+
let n = H(e, t);
|
|
255
255
|
if (n.length === 0) return;
|
|
256
256
|
let r = n.map((e) => ` · flag "${e.flag}" → experiment "${e.experiment}": ${e.problem}`);
|
|
257
257
|
throw Error(`@voltro/plugin-flags: ${n.length} flag→experiment link problem(s):\n${r.join("\n")}`);
|
|
258
|
-
},
|
|
259
|
-
let n =
|
|
258
|
+
}, W = 864e5, G = (e) => typeof e == "number" ? e : Date.parse(e), K = (e, t) => {
|
|
259
|
+
let n = C(e);
|
|
260
260
|
if (n.enabled === !1) return {
|
|
261
261
|
shape: "constantOff",
|
|
262
262
|
reason: "enabled: false — resolves false for every caller"
|
|
263
263
|
};
|
|
264
264
|
if (n.schedule) {
|
|
265
265
|
let { activateAt: e, deactivateAt: r } = n.schedule;
|
|
266
|
-
if (r != null && t >=
|
|
266
|
+
if (r != null && t >= G(r)) return {
|
|
267
267
|
shape: "expired",
|
|
268
268
|
reason: `schedule.deactivateAt (${String(r)}) has passed — the flag can never be on again`
|
|
269
269
|
};
|
|
270
|
-
if (e != null && t <
|
|
270
|
+
if (e != null && t < G(e)) return {
|
|
271
271
|
shape: "notYetActive",
|
|
272
272
|
reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
|
|
273
273
|
};
|
|
@@ -277,7 +277,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
277
277
|
shape: "constantOff",
|
|
278
278
|
reason: `rollout: ${r} — nobody is in the rollout`
|
|
279
279
|
};
|
|
280
|
-
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 <
|
|
280
|
+
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 < G(n.schedule.deactivateAt));
|
|
281
281
|
return r >= 100 && !i && !a && !o ? {
|
|
282
282
|
shape: "constantOn",
|
|
283
283
|
reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
|
|
@@ -290,14 +290,14 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
290
290
|
o ? "a live schedule" : void 0
|
|
291
291
|
].filter((e) => e !== void 0).join(", ")})`
|
|
292
292
|
};
|
|
293
|
-
},
|
|
293
|
+
}, ce = [
|
|
294
294
|
"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.",
|
|
295
295
|
"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.",
|
|
296
296
|
"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.",
|
|
297
297
|
"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.",
|
|
298
298
|
"\"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."
|
|
299
|
-
],
|
|
300
|
-
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n *
|
|
299
|
+
], le = (e) => {
|
|
300
|
+
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n * W, i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o;
|
|
301
301
|
for (let t of e.usage) {
|
|
302
302
|
let e = t.lastAt.getTime();
|
|
303
303
|
if (!Number.isFinite(e)) continue;
|
|
@@ -305,7 +305,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
305
305
|
let n = t.source === "bulk" ? a : i, r = n.get(t.flag);
|
|
306
306
|
(r === void 0 || e > r) && n.set(t.flag, e);
|
|
307
307
|
}
|
|
308
|
-
let s = o === void 0 ? 0 : Math.floor((t - o) /
|
|
308
|
+
let s = o === void 0 ? 0 : Math.floor((t - o) / W);
|
|
309
309
|
return {
|
|
310
310
|
coverage: {
|
|
311
311
|
tracking: e.tracking,
|
|
@@ -315,7 +315,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
315
315
|
staleAfterDays: n
|
|
316
316
|
},
|
|
317
317
|
findings: Object.entries(e.flags).map(([o, c]) => {
|
|
318
|
-
let { shape: l, reason: u } =
|
|
318
|
+
let { shape: l, reason: u } = K(c, t), d = i.get(o), f = a.get(o), p = [u], m;
|
|
319
319
|
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"));
|
|
320
320
|
let h = l === "constantOn" || l === "constantOff" || l === "expired";
|
|
321
321
|
return h && p.push("the definition alone makes this flag a constant — it selects nothing"), {
|
|
@@ -330,23 +330,23 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
330
330
|
why: p
|
|
331
331
|
};
|
|
332
332
|
}),
|
|
333
|
-
limits:
|
|
333
|
+
limits: ce
|
|
334
334
|
};
|
|
335
|
-
}, q = "_voltro_feature_flag_usage", J =
|
|
336
|
-
id:
|
|
337
|
-
flag:
|
|
338
|
-
day:
|
|
339
|
-
source:
|
|
340
|
-
lastAt:
|
|
335
|
+
}, q = "_voltro_feature_flag_usage", J = b(q, {
|
|
336
|
+
id: m({ prefix: "flagusg" }),
|
|
337
|
+
flag: x(),
|
|
338
|
+
day: x(),
|
|
339
|
+
source: x(),
|
|
340
|
+
lastAt: S()
|
|
341
341
|
}).unique([
|
|
342
342
|
"flag",
|
|
343
343
|
"day",
|
|
344
344
|
"source"
|
|
345
|
-
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]),
|
|
345
|
+
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]), ue = (e) => e.toISOString().slice(0, 10), de = (e) => {
|
|
346
346
|
let t = 2166136261;
|
|
347
347
|
for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
|
|
348
348
|
return (t >>> 0).toString(16).padStart(8, "0");
|
|
349
|
-
},
|
|
349
|
+
}, fe = (e, t, n) => `flagusg_${t.replace(/-/g, "")}_${n === "targeted" ? "t" : "b"}_${de(e)}`, Y = new class {
|
|
350
350
|
on = !1;
|
|
351
351
|
pending = /* @__PURE__ */ new Map();
|
|
352
352
|
liveCounts = /* @__PURE__ */ new Map();
|
|
@@ -384,12 +384,12 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
384
384
|
reset() {
|
|
385
385
|
this.on = !1, this.pending.clear(), this.liveCounts.clear();
|
|
386
386
|
}
|
|
387
|
-
}(), X = (e, t, n) => Y.record(e, t, n),
|
|
387
|
+
}(), X = (e, t, n) => Y.record(e, t, n), pe = (e) => e === "bulk" ? "bulk" : "targeted", me = (e) => ({
|
|
388
388
|
upsertMany: async (t) => {
|
|
389
389
|
for (let n of t) {
|
|
390
|
-
let t =
|
|
390
|
+
let t = ue(n.lastAt);
|
|
391
391
|
await e.upsert(q, {
|
|
392
|
-
id:
|
|
392
|
+
id: fe(n.flag, t, n.source),
|
|
393
393
|
flag: n.flag,
|
|
394
394
|
day: t,
|
|
395
395
|
source: n.source,
|
|
@@ -401,13 +401,13 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
401
401
|
] });
|
|
402
402
|
}
|
|
403
403
|
},
|
|
404
|
-
load: async () => (await e.query(
|
|
404
|
+
load: async () => (await e.query(_(J).descriptor)).map((e) => ({
|
|
405
405
|
flag: String(e.flag),
|
|
406
406
|
day: String(e.day),
|
|
407
|
-
source:
|
|
407
|
+
source: pe(e.source),
|
|
408
408
|
lastAt: e.lastAt instanceof Date ? e.lastAt : new Date(String(e.lastAt))
|
|
409
409
|
}))
|
|
410
|
-
}),
|
|
410
|
+
}), he = 3e5, ge = (e) => {
|
|
411
411
|
let t = e.intervalMs ?? 3e5, n = !1, r = async () => {
|
|
412
412
|
let t = Y.drain();
|
|
413
413
|
if (t.length !== 0) try {
|
|
@@ -431,9 +431,9 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
431
431
|
n || (n = !0, clearInterval(i), await r());
|
|
432
432
|
}
|
|
433
433
|
};
|
|
434
|
-
},
|
|
435
|
-
let n =
|
|
436
|
-
return
|
|
434
|
+
}, _e = (e = process.env, t = 90) => {
|
|
435
|
+
let n = y(e.VOLTRO_FLAG_USAGE_TTL_HOURS, 24 * t);
|
|
436
|
+
return v({
|
|
437
437
|
table: q,
|
|
438
438
|
timeColumn: "lastAt",
|
|
439
439
|
source: "plugin",
|
|
@@ -441,88 +441,88 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
441
441
|
envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
|
|
442
442
|
}), n;
|
|
443
443
|
}, Z = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, Q = (e, t) => {
|
|
444
|
-
let n =
|
|
445
|
-
return X(t, "targeted"), n !== void 0 &&
|
|
446
|
-
},
|
|
447
|
-
let n =
|
|
448
|
-
return X(t.key, "targeted"), n === void 0 ? t.defaultValue :
|
|
449
|
-
},
|
|
450
|
-
let n =
|
|
451
|
-
return X(t.key, "targeted"), n === void 0 ? null :
|
|
452
|
-
},
|
|
453
|
-
let
|
|
444
|
+
let n = R.get(t);
|
|
445
|
+
return X(t, "targeted"), n !== void 0 && A(t, n, Z(e));
|
|
446
|
+
}, ve = (e, t) => Q(e, t) ? l.void : l.fail(new c({ flag: t })), ye = (e, t) => {
|
|
447
|
+
let n = R.get(t.key);
|
|
448
|
+
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : i(t, N(t.key, n, Z(e)));
|
|
449
|
+
}, be = (e, t) => {
|
|
450
|
+
let n = R.get(t.key);
|
|
451
|
+
return X(t.key, "targeted"), n === void 0 ? null : N(t.key, n, Z(e)).name;
|
|
452
|
+
}, xe = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), $ = e, Se = (e = {}) => {
|
|
453
|
+
let i = e.gatedBy ?? {}, o = d({
|
|
454
454
|
base: $,
|
|
455
|
-
alias:
|
|
456
|
-
instance:
|
|
457
|
-
}),
|
|
458
|
-
for (let
|
|
459
|
-
if (f.has(
|
|
460
|
-
if (
|
|
461
|
-
f.set(
|
|
455
|
+
alias: e.alias,
|
|
456
|
+
instance: e.name
|
|
457
|
+
}), s = e.typedFlags ?? [], f = /* @__PURE__ */ new Map();
|
|
458
|
+
for (let t of s) {
|
|
459
|
+
if (f.has(t.key)) throw Error(`${$}: two typedFlags declare the key ${JSON.stringify(t.key)}`);
|
|
460
|
+
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.`);
|
|
461
|
+
f.set(t.key, t);
|
|
462
462
|
}
|
|
463
|
-
|
|
464
|
-
let p = { ...
|
|
465
|
-
for (let e of
|
|
466
|
-
let m =
|
|
463
|
+
s.some((e) => e.experiment !== void 0) && U(s, e.experiments ?? []);
|
|
464
|
+
let p = { ...e.flags ?? {} };
|
|
465
|
+
for (let e of s) p[e.key] = e.definition;
|
|
466
|
+
let m = e.store === "postgres", h, g, _, v, y = [], b = e.usage ?? {}, x = b.track ?? m;
|
|
467
467
|
if (x && !m) 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.`);
|
|
468
468
|
let S = b.staleAfterDays ?? 30, C = b.retentionDays ?? 90;
|
|
469
469
|
if (S > C) throw Error(`${$}: usage.staleAfterDays (${S}) exceeds usage.retentionDays (${C}), so the observation window can never be long enough to prove a flag stale and every verdict would be withheld.`);
|
|
470
|
-
let
|
|
471
|
-
let t =
|
|
470
|
+
let w = (e) => {
|
|
471
|
+
let t = re(p, e), n = [];
|
|
472
472
|
for (let [e, r] of f) {
|
|
473
|
-
let
|
|
474
|
-
if (
|
|
475
|
-
let o =
|
|
473
|
+
let i = t[e];
|
|
474
|
+
if (i === void 0) continue;
|
|
475
|
+
let o = a(r, typeof i == "boolean" ? { enabled: i } : i);
|
|
476
476
|
o.ok || (n.push(o.refusal), t[e] = r.definition);
|
|
477
477
|
}
|
|
478
|
-
y = n,
|
|
479
|
-
},
|
|
480
|
-
h &&
|
|
481
|
-
},
|
|
482
|
-
if (
|
|
483
|
-
for (let [t, n] of Object.entries(
|
|
478
|
+
y = n, R.set(t);
|
|
479
|
+
}, T = async () => {
|
|
480
|
+
h && w(await h.loadOverrides());
|
|
481
|
+
}, ee = (e) => {
|
|
482
|
+
if (i[e] !== void 0) return i[e];
|
|
483
|
+
for (let [t, n] of Object.entries(i)) if (t !== e && xe(t, e)) return n;
|
|
484
484
|
}, E = (e, t) => {
|
|
485
|
-
let n =
|
|
486
|
-
return n === void 0 || Q({ subject: t.subject }, n) ? e :
|
|
487
|
-
}, D = Object.keys(
|
|
485
|
+
let n = ee(t.tag);
|
|
486
|
+
return n === void 0 || Q({ subject: t.subject }, n) ? e : l.fail(new c({ flag: n }));
|
|
487
|
+
}, D = Object.keys(i).length > 0, O = (e) => {
|
|
488
488
|
for (let t of e) X(t, "bulk");
|
|
489
489
|
}, k = {
|
|
490
|
-
...
|
|
490
|
+
...t,
|
|
491
491
|
description: "Resolve every feature flag for the calling subject.",
|
|
492
|
-
execute: (e, t) =>
|
|
493
|
-
let e =
|
|
494
|
-
return O(Object.keys(e)),
|
|
492
|
+
execute: (e, t) => l.sync(() => {
|
|
493
|
+
let e = R.all();
|
|
494
|
+
return O(Object.keys(e)), te(e, t.request.subject ?? {});
|
|
495
495
|
})
|
|
496
|
-
},
|
|
497
|
-
...
|
|
496
|
+
}, A = {
|
|
497
|
+
...n,
|
|
498
498
|
description: "Resolve every feature flag to its served variant for the calling subject.",
|
|
499
|
-
execute: (e, t) =>
|
|
500
|
-
let e =
|
|
501
|
-
return O(Object.keys(e)),
|
|
499
|
+
execute: (e, t) => l.sync(() => {
|
|
500
|
+
let e = R.all();
|
|
501
|
+
return O(Object.keys(e)), ne(e, t.request.subject ?? {});
|
|
502
502
|
})
|
|
503
|
-
},
|
|
503
|
+
}, j = (e) => ({
|
|
504
504
|
kind: "json",
|
|
505
505
|
data: e
|
|
506
|
-
}),
|
|
506
|
+
}), M = (e, t) => ({
|
|
507
507
|
kind: "json",
|
|
508
508
|
status: e,
|
|
509
509
|
data: { error: t }
|
|
510
|
-
}),
|
|
510
|
+
}), N = async () => {
|
|
511
511
|
let e = [];
|
|
512
512
|
if (x && _) try {
|
|
513
513
|
e = await _.load();
|
|
514
514
|
} catch {
|
|
515
515
|
e = [];
|
|
516
516
|
}
|
|
517
|
-
return
|
|
518
|
-
flags:
|
|
517
|
+
return le({
|
|
518
|
+
flags: R.all(),
|
|
519
519
|
usage: e,
|
|
520
520
|
tracking: x,
|
|
521
521
|
liveCounts: Y.liveTargetedCounts(),
|
|
522
522
|
staleAfterDays: S,
|
|
523
523
|
retentionDays: C
|
|
524
524
|
});
|
|
525
|
-
},
|
|
525
|
+
}, P = () => {
|
|
526
526
|
let e = {};
|
|
527
527
|
for (let [t, n] of f) e[t] = {
|
|
528
528
|
valueType: n.valueType,
|
|
@@ -530,17 +530,17 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
530
530
|
experiment: n.experiment ?? null
|
|
531
531
|
};
|
|
532
532
|
return e;
|
|
533
|
-
},
|
|
533
|
+
}, F = [
|
|
534
534
|
{
|
|
535
535
|
method: "GET",
|
|
536
536
|
path: "/list",
|
|
537
537
|
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.",
|
|
538
|
-
handler: () =>
|
|
539
|
-
let e = await
|
|
540
|
-
return
|
|
541
|
-
flags:
|
|
538
|
+
handler: () => l.promise(async () => {
|
|
539
|
+
let e = await N();
|
|
540
|
+
return j({
|
|
541
|
+
flags: V(R.all()),
|
|
542
542
|
lifecycle: e,
|
|
543
|
-
typed:
|
|
543
|
+
typed: P(),
|
|
544
544
|
overrideRefusals: y
|
|
545
545
|
});
|
|
546
546
|
})
|
|
@@ -549,26 +549,26 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
549
549
|
method: "POST",
|
|
550
550
|
path: "/toggle",
|
|
551
551
|
description: "Flip a flag's kill-switch on/off (live registry; durable on the postgres tier). Logs an audit row.",
|
|
552
|
-
handler: (e) =>
|
|
552
|
+
handler: (e) => l.gen(function* () {
|
|
553
553
|
let t;
|
|
554
554
|
try {
|
|
555
555
|
t = JSON.parse(e.body || "{}");
|
|
556
556
|
} catch {
|
|
557
|
-
return
|
|
557
|
+
return M(400, "invalid JSON body");
|
|
558
558
|
}
|
|
559
|
-
if (!t.key || typeof t.enabled != "boolean") return
|
|
560
|
-
let n =
|
|
561
|
-
if (n === null) return
|
|
562
|
-
let r = h ?
|
|
563
|
-
return h && r !== void 0 && (yield*
|
|
559
|
+
if (!t.key || typeof t.enabled != "boolean") return M(400, "key (string) + enabled (boolean) required");
|
|
560
|
+
let n = B(t.key, t.enabled);
|
|
561
|
+
if (n === null) return M(404, `unknown flag "${t.key}"`);
|
|
562
|
+
let r = h ? R.get(t.key) : void 0;
|
|
563
|
+
return h && r !== void 0 && (yield* l.promise(() => h.upsertOverride(ie(t.key, r)))), g && (yield* l.promise(() => g.record({
|
|
564
564
|
flag: t.key,
|
|
565
565
|
enabled: t.enabled,
|
|
566
566
|
previousEnabled: n,
|
|
567
567
|
actorId: typeof t.actor == "string" ? t.actor : null,
|
|
568
568
|
at: /* @__PURE__ */ new Date()
|
|
569
|
-
}))),
|
|
569
|
+
}))), j({
|
|
570
570
|
ok: !0,
|
|
571
|
-
flags:
|
|
571
|
+
flags: V(R.all())
|
|
572
572
|
});
|
|
573
573
|
})
|
|
574
574
|
},
|
|
@@ -576,16 +576,16 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
576
576
|
method: "GET",
|
|
577
577
|
path: "/audit",
|
|
578
578
|
description: "The kill-switch flip audit trail, newest-first. `?flag=<key>` filters; `?limit=<n>` caps (postgres tier only).",
|
|
579
|
-
handler: (e) =>
|
|
580
|
-
if (!g) return
|
|
579
|
+
handler: (e) => l.gen(function* () {
|
|
580
|
+
if (!g) return j({
|
|
581
581
|
audit: [],
|
|
582
582
|
note: "audit trail requires store: \"postgres\""
|
|
583
583
|
});
|
|
584
|
-
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*
|
|
584
|
+
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(() => g.list({
|
|
585
585
|
...n == null ? {} : { flag: n },
|
|
586
586
|
limit: i
|
|
587
587
|
}));
|
|
588
|
-
return
|
|
588
|
+
return j({ audit: a.map((e) => ({
|
|
589
589
|
flag: e.flag,
|
|
590
590
|
enabled: e.enabled,
|
|
591
591
|
previousEnabled: e.previousEnabled,
|
|
@@ -594,7 +594,7 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
594
594
|
})) });
|
|
595
595
|
})
|
|
596
596
|
}
|
|
597
|
-
],
|
|
597
|
+
], z = [
|
|
598
598
|
...D ? [
|
|
599
599
|
"rpc:intercept:mutation",
|
|
600
600
|
"rpc:intercept:query",
|
|
@@ -604,16 +604,16 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
604
604
|
"inspect:read",
|
|
605
605
|
"inspect:write"
|
|
606
606
|
];
|
|
607
|
-
return
|
|
607
|
+
return u({
|
|
608
608
|
name: o,
|
|
609
609
|
baseName: $,
|
|
610
610
|
description: "Feature flags — targeting, % rollout, kill-switch, declarative gating + client evaluation.",
|
|
611
|
-
permissions:
|
|
612
|
-
routes: [k,
|
|
613
|
-
rpcClientDescriptors:
|
|
614
|
-
inspectEndpoints:
|
|
611
|
+
permissions: z,
|
|
612
|
+
routes: [k, A],
|
|
613
|
+
rpcClientDescriptors: r,
|
|
614
|
+
inspectEndpoints: F,
|
|
615
615
|
errorSchemas: [{
|
|
616
|
-
schema:
|
|
616
|
+
schema: c,
|
|
617
617
|
import: {
|
|
618
618
|
module: "@voltro/plugin-flags/errors",
|
|
619
619
|
name: "FlagDisabled"
|
|
@@ -625,38 +625,38 @@ var S = (e) => typeof e == "boolean" ? { enabled: e } : e, C = (e, t) => {
|
|
|
625
625
|
interceptAction: E
|
|
626
626
|
} : {},
|
|
627
627
|
...m ? { extendSchema: { tables: x ? [
|
|
628
|
-
|
|
629
|
-
|
|
628
|
+
I,
|
|
629
|
+
L,
|
|
630
630
|
J
|
|
631
|
-
] : [
|
|
631
|
+
] : [I, L] } } : {},
|
|
632
632
|
...m ? { bindDataStore: (e) => {
|
|
633
|
-
h = ae(e), g = se(e), x && (_ =
|
|
633
|
+
h = ae(e), g = se(e), x && (_ = me(e));
|
|
634
634
|
} } : {},
|
|
635
|
-
...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ?
|
|
636
|
-
onActivate: (
|
|
637
|
-
|
|
638
|
-
let
|
|
639
|
-
x && _ && (
|
|
635
|
+
...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? l.tryPromise(() => T()) : l.void } : {},
|
|
636
|
+
onActivate: (t) => l.gen(function* () {
|
|
637
|
+
R.set(p), m && h && (yield* l.promise(T));
|
|
638
|
+
let n;
|
|
639
|
+
x && _ && (n = _e(process.env, C), Y.enable(), v = ge({
|
|
640
640
|
store: _,
|
|
641
641
|
intervalMs: b.flushIntervalMs ?? 3e5,
|
|
642
|
-
log: { warn: (
|
|
642
|
+
log: { warn: (e, n) => t.logger.warn(e, n) }
|
|
643
643
|
}));
|
|
644
|
-
for (let
|
|
645
|
-
flag:
|
|
646
|
-
reason:
|
|
644
|
+
for (let e of y) t.logger.warn("feature flag override REFUSED — the code-declared definition stands", {
|
|
645
|
+
flag: e.key,
|
|
646
|
+
reason: e.reason
|
|
647
647
|
});
|
|
648
|
-
|
|
648
|
+
t.logger.info("feature flags active", {
|
|
649
649
|
count: Object.keys(p).length,
|
|
650
650
|
typed: f.size,
|
|
651
|
-
gated: Object.keys(
|
|
652
|
-
store:
|
|
653
|
-
usage: x ? `tracking (stale after ${S}d, observations kept ${Math.round((
|
|
651
|
+
gated: Object.keys(i).length,
|
|
652
|
+
store: e.store ?? "memory",
|
|
653
|
+
usage: x ? `tracking (stale after ${S}d, observations kept ${Math.round((n ?? 0) / 864e5)}d)` : "off — no dead-flag evidence is collected"
|
|
654
654
|
});
|
|
655
655
|
}),
|
|
656
|
-
onDeactivate: () =>
|
|
657
|
-
await v?.stop(), v = void 0, Y.disable(),
|
|
656
|
+
onDeactivate: () => l.promise(async () => {
|
|
657
|
+
await v?.stop(), v = void 0, Y.disable(), R.set({});
|
|
658
658
|
})
|
|
659
659
|
});
|
|
660
660
|
};
|
|
661
661
|
//#endregion
|
|
662
|
-
export {
|
|
662
|
+
export { he as DEFAULT_FLAG_USAGE_FLUSH_MS, ce as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, c as FlagDisabled, U as assertFlagExperimentLinks, H as checkFlagExperimentLinks, K as classifyShape, me as dataStoreFlagUsageStore, s as defineFlag, V as describeFlags, te as evaluateAll, ne as evaluateAllVariants, A as evaluateFlag, k as evaluateSchedule, N as evaluateVariant, I as featureFlagsTable, L as flagAuditTable, le as flagLifecycleReport, R as flagRegistry, Y as flagUsageBuffer, fe as flagUsageRowId, J as flagUsageTable, ye as flagValue, be as flagVariant, Se as flagsPlugin, B as flipFlagEnabled, a as gateOverride, Q as isFlagEnabled, o as isTypedFlag, re as mergeOverrides, X as recordFlagEvaluation, _e as registerFlagUsageRetention, ve as requireFlag, M as resolveVariant, z as setFlagEnabled, ge as startFlagUsageFlush, C as toDefinition, i as typedValueOf, ue as utcDay };
|