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