@voltro/plugin-flags 0.32.0 → 0.34.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/dist/index.js CHANGED
@@ -1,24 +1,25 @@
1
1
  import { evaluateDescriptor as e, evaluateVariantsDescriptor as t, flagsRpcClientImports as n } from "./rpc.js";
2
- import { FlagDisabled as r } from "./errors.js";
3
- import { Effect as i } from "effect";
4
- import { definePlugin as a } from "@voltro/protocol";
5
- import { boolean as o, id as s, integer as c, json as l, queryFor as u, table as d, text as f, timestamp as p } from "@voltro/database";
2
+ import { i as r, n as i, r as a, t as o } from "./define-D9Eet7wA.js";
3
+ import { FlagDisabled as s } from "./errors.js";
4
+ import { Effect as c } from "effect";
5
+ import { definePlugin as l, pluginInstanceName as u } from "@voltro/protocol";
6
+ import { boolean as d, id as f, integer as p, json as m, queryFor as h, registerRetention as g, retentionTtlMsFromEnv as _, table as v, text as y, timestamp as b } from "@voltro/database";
6
7
  //#region src/evaluate.ts
7
- var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
8
+ var x = (e) => typeof e == "boolean" ? { enabled: e } : e, S = (e, t) => {
8
9
  let n = 2166136261, r = `${e}:${t}`;
9
10
  for (let e = 0; e < r.length; e++) n ^= r.charCodeAt(e), n = Math.imul(n, 16777619);
10
11
  return n >>> 0;
11
- }, g = (e, t) => h(e, t) % 100, _ = (e, t) => h(`${e}:variant`, t) % 1e4, v = (e, t) => {
12
+ }, C = (e, t) => S(e, t) % 100, ee = (e, t) => S(`${e}:variant`, t) % 1e4, w = (e, t) => {
12
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;
13
14
  if (e.metadata) {
14
15
  let n = t.metadata ?? {};
15
16
  for (let [t, r] of Object.entries(e.metadata)) if (String(n[t]) !== r) return !1;
16
17
  }
17
18
  return !0;
18
- }, y = (e) => typeof e == "number" ? e : Date.parse(e), b = (e, t) => (e.rolloutBy === "tenant" ? t.tenantId : t.id) ?? "anon", x = (e, t) => {
19
- if (e.activateAt != null && t < y(e.activateAt) || e.deactivateAt != null && t >= y(e.deactivateAt)) return { active: !1 };
19
+ }, T = (e) => typeof e == "number" ? e : Date.parse(e), E = (e, t) => (e.rolloutBy === "tenant" ? t.tenantId : t.id) ?? "anon", D = (e, t) => {
20
+ if (e.activateAt != null && t < T(e.activateAt) || e.deactivateAt != null && t >= T(e.deactivateAt)) return { active: !1 };
20
21
  if (e.ramp) {
21
- let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o = y(i), s = y(a);
22
+ let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o = T(i), s = T(a);
22
23
  if (t <= o) return {
23
24
  active: !0,
24
25
  rollout: n
@@ -34,71 +35,71 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
34
35
  };
35
36
  }
36
37
  return { active: !0 };
37
- }, S = (e, t, n, r = Date.now()) => {
38
- let i = m(t);
38
+ }, O = (e, t, n, r = Date.now()) => {
39
+ let i = x(t);
39
40
  if (i.enabled === !1) return !1;
40
41
  let a;
41
42
  if (i.schedule) {
42
- let e = x(i.schedule, r);
43
+ let e = D(i.schedule, r);
43
44
  if (!e.active) return !1;
44
45
  a = e.rollout;
45
46
  }
46
- if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) => v(e, n))) return !1;
47
+ if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) => w(e, n))) return !1;
47
48
  let o = a ?? i.rollout ?? 100;
48
49
  if (o < 100 && o > 0) {
49
- if (!(g(e, b(i, n)) < o)) return !1;
50
+ if (!(C(e, E(i, n)) < o)) return !1;
50
51
  } else if (o <= 0) return !1;
51
- return i.variants && i.variants.length > 0 && i.offVariant != null ? w(e, i, n).name !== i.offVariant : !0;
52
- }, C = (e) => {
52
+ return i.variants && i.variants.length > 0 && i.offVariant != null ? k(e, i, n).name !== i.offVariant : !0;
53
+ }, te = (e) => {
53
54
  let t = e.reduce((e, t) => e + (t.weight ?? 1), 0) || e.length, n = 0;
54
55
  return e.map((r, i) => (n += r.weight ?? 1, i === e.length - 1 ? 1e4 : Math.round(n / t * 1e4)));
55
- }, w = (e, t, n) => {
56
+ }, k = (e, t, n) => {
56
57
  let r = t.variants ?? [];
57
58
  if (r.length === 0) throw Error(`flag "${e}" has no variants`);
58
- let i = _(e, b(t, n)), a = C(r);
59
+ let i = ee(e, E(t, n)), a = te(r);
59
60
  for (let e = 0; e < r.length; e++) if (i < a[e]) return r[e];
60
61
  return r[r.length - 1];
61
- }, T = (e, t, n, r = Date.now()) => {
62
- let i = m(t), a = S(e, t, n, r);
62
+ }, A = (e, t, n, r = Date.now()) => {
63
+ let i = x(t), a = O(e, t, n, r);
63
64
  if (!i.variants || i.variants.length === 0) return {
64
65
  name: a ? "on" : "off",
65
66
  value: a,
66
67
  enabled: a
67
68
  };
68
- let o = w(e, i, n);
69
+ let o = k(e, i, n);
69
70
  return {
70
71
  name: o.name,
71
72
  value: o.value,
72
73
  enabled: a
73
74
  };
74
- }, E = (e, t, n = Date.now()) => {
75
+ }, ne = (e, t, n = Date.now()) => {
75
76
  let r = {};
76
- for (let [i, a] of Object.entries(e)) r[i] = S(i, a, t, n);
77
+ for (let [i, a] of Object.entries(e)) r[i] = O(i, a, t, n);
77
78
  return r;
78
- }, D = (e, t, n = Date.now()) => {
79
+ }, re = (e, t, n = Date.now()) => {
79
80
  let r = {};
80
- for (let [i, a] of Object.entries(e)) r[i] = T(i, a, t, n);
81
+ for (let [i, a] of Object.entries(e)) r[i] = A(i, a, t, n);
81
82
  return r;
82
- }, O = "_voltro_feature_flag_audit", k = "_voltro_feature_flags", A = d(k, {
83
- id: s({ prefix: "flag" }),
84
- key: f().unique(),
85
- enabled: o().default(!0),
86
- rollout: c().nullable(),
87
- rolloutBy: f().nullable(),
88
- targeting: l().nullable(),
89
- variants: l().nullable(),
90
- offVariant: f().nullable(),
91
- schedule: l().nullable(),
92
- description: f().nullable(),
93
- updatedAt: p().default("now").onUpdate("now")
94
- }), j = d(O, {
95
- id: s({ prefix: "flagaud" }),
96
- flag: f(),
97
- enabled: o(),
98
- previousEnabled: o().nullable(),
99
- actorId: f().nullable(),
100
- at: p().default("now")
101
- }).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), M = (e, t) => {
83
+ }, j = "_voltro_feature_flag_audit", M = "_voltro_feature_flags", N = v(M, {
84
+ id: f({ prefix: "flag" }),
85
+ key: y().unique(),
86
+ enabled: d().default(!0),
87
+ rollout: p().nullable(),
88
+ rolloutBy: y().nullable(),
89
+ targeting: m().nullable(),
90
+ variants: m().nullable(),
91
+ offVariant: y().nullable(),
92
+ schedule: m().nullable(),
93
+ description: y().nullable(),
94
+ updatedAt: b().default("now").onUpdate("now")
95
+ }), P = v(j, {
96
+ id: f({ prefix: "flagaud" }),
97
+ flag: y(),
98
+ enabled: d(),
99
+ previousEnabled: d().nullable(),
100
+ actorId: y().nullable(),
101
+ at: b().default("now")
102
+ }).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), F = (e, t) => {
102
103
  let n = { ...e };
103
104
  for (let e of t) {
104
105
  let t = {
@@ -114,7 +115,7 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
114
115
  n[e.key] = t;
115
116
  }
116
117
  return n;
117
- }, N = new class {
118
+ }, I = new class {
118
119
  flags = {};
119
120
  set(e) {
120
121
  this.flags = e;
@@ -125,19 +126,19 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
125
126
  get(e) {
126
127
  return this.flags[e];
127
128
  }
128
- }(), P = (e, t) => F(e, t) !== null, F = (e, t) => {
129
- let n = N.get(e);
129
+ }(), L = (e, t) => R(e, t) !== null, R = (e, t) => {
130
+ let n = I.get(e);
130
131
  if (n === void 0) return null;
131
- let r = m(n), i = r.enabled !== !1;
132
- return N.set({
133
- ...N.all(),
132
+ let r = x(n), i = r.enabled !== !1;
133
+ return I.set({
134
+ ...I.all(),
134
135
  [e]: {
135
136
  ...r,
136
137
  enabled: t
137
138
  }
138
139
  }), i;
139
- }, I = (e) => Object.entries(e).map(([e, t]) => {
140
- let n = m(t);
140
+ }, z = (e) => Object.entries(e).map(([e, t]) => {
141
+ let n = x(t);
141
142
  return {
142
143
  key: e,
143
144
  enabled: n.enabled !== !1,
@@ -146,8 +147,8 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
146
147
  scheduled: n.schedule != null,
147
148
  description: n.description
148
149
  };
149
- }), L = (e, t) => {
150
- let n = m(t);
150
+ }), ie = (e, t) => {
151
+ let n = x(t);
151
152
  return {
152
153
  key: e,
153
154
  enabled: n.enabled !== !1,
@@ -159,8 +160,8 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
159
160
  schedule: n.schedule ?? null,
160
161
  description: n.description ?? null
161
162
  };
162
- }, R = (e) => ({
163
- loadOverrides: async () => (await e.query(u(A).descriptor)).map((e) => ({
163
+ }, ae = (e) => ({
164
+ loadOverrides: async () => (await e.query(h(N).descriptor)).map((e) => ({
164
165
  key: String(e.key),
165
166
  enabled: e.enabled === !0 || e.enabled === 1,
166
167
  rollout: e.rollout == null ? null : Number(e.rollout),
@@ -185,17 +186,17 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
185
186
  description: t.description,
186
187
  updatedAt: /* @__PURE__ */ new Date()
187
188
  };
188
- await e.upsert(k, n, { conflictColumns: ["key"] });
189
+ await e.upsert(M, n, { conflictColumns: ["key"] });
189
190
  }
190
- }), z = (e) => ({
191
+ }), oe = (e) => ({
191
192
  flag: String(e.flag),
192
193
  enabled: e.enabled === !0 || e.enabled === 1,
193
194
  previousEnabled: e.previousEnabled == null ? null : e.previousEnabled === !0 || e.previousEnabled === 1,
194
195
  actorId: e.actorId ?? null,
195
196
  at: e.at instanceof Date ? e.at : new Date(String(e.at))
196
- }), B = (e) => ({
197
+ }), se = (e) => ({
197
198
  record: async (t) => {
198
- await e.insert(O, {
199
+ await e.insert(j, {
199
200
  id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
200
201
  flag: t.flag,
201
202
  enabled: t.enabled,
@@ -205,69 +206,369 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
205
206
  });
206
207
  },
207
208
  list: async (t) => {
208
- let n = u(j).orderBy("at", "desc");
209
+ let n = h(P).orderBy("at", "desc");
209
210
  t?.limit != null && (n = n.limit(t.limit));
210
- let r = (await e.query(n.descriptor)).map(z);
211
+ let r = (await e.query(n.descriptor)).map(oe);
211
212
  return t?.flag == null ? r : r.filter((e) => e.flag === t.flag);
212
213
  }
213
- }), V = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, H = (e, t) => {
214
- let n = N.get(t);
215
- return n !== void 0 && S(t, n, V(e));
216
- }, U = (e, t) => H(e, t) ? i.void : i.fail(new r({ flag: t })), W = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), G = (o = {}) => {
217
- let s = { ...o.flags ?? {} }, c = o.gatedBy ?? {}, l = o.name ? `@voltro/plugin-flags#${o.name}` : "@voltro/plugin-flags", u = o.store === "postgres", d, f, p = async () => {
218
- d && N.set(M(s, await d.loadOverrides()));
219
- }, m = (e) => {
220
- if (c[e] !== void 0) return c[e];
221
- for (let [t, n] of Object.entries(c)) if (t !== e && W(t, e)) return n;
222
- }, h = (e, t) => {
223
- let n = m(t.tag);
224
- return n === void 0 || H({ subject: t.subject }, n) ? e : i.fail(new r({ flag: n }));
225
- }, g = Object.keys(c).length > 0, _ = {
214
+ }), B = (e, t) => {
215
+ let n = new Map(t.map((e) => [e.name, e])), r = [];
216
+ for (let t of e) {
217
+ let e = t.experiment;
218
+ if (e === void 0) continue;
219
+ let i = n.get(e);
220
+ if (i === void 0) {
221
+ r.push({
222
+ flag: t.key,
223
+ experiment: e,
224
+ problem: `no experiment named ${JSON.stringify(e)} was passed to flagsPlugin({ experiments }). Pass the defineExperiment() definitions the app declares, or drop the \`experiment\` link.`
225
+ });
226
+ continue;
227
+ }
228
+ (i.variantFrom === void 0 || i.variantFrom === null) && r.push({
229
+ flag: t.key,
230
+ experiment: e,
231
+ problem: `the experiment assigns its own arms (\`subject\` mode), so it would measure a split this flag never served — the two bucketings are independent hashes. Persist the served arm (\`flagVariant(ctx, ${JSON.stringify(t.key)})\`) on the watched table and declare the experiment with \`variantFrom: <that column>\`.`
232
+ }), typeof i.holdout == "number" && i.holdout > 0 && r.push({
233
+ flag: t.key,
234
+ experiment: e,
235
+ problem: `holdout ${i.holdout} is carved at ASSIGNMENT, and this flag does the assigning — the experiment can never place a row there. Declare the holdout as a flag variant instead.`
236
+ });
237
+ let a = new Set(t.variants.map((e) => e.name)), o = new Set(i.variants.map((e) => e.name));
238
+ if (a.size === 0) r.push({
239
+ flag: t.key,
240
+ experiment: e,
241
+ problem: "the flag declares no variants, so it serves one value to everyone and there is nothing to compare."
242
+ });
243
+ else {
244
+ let n = [...a].filter((e) => !o.has(e)), i = [...o].filter((e) => !a.has(e));
245
+ (n.length > 0 || i.length > 0) && r.push({
246
+ flag: t.key,
247
+ experiment: e,
248
+ problem: `the arms disagree — ${n.length > 0 ? `served by the flag but not declared by the experiment: ${n.join(", ")}` : ""}${n.length > 0 && i.length > 0 ? "; " : ""}${i.length > 0 ? `declared by the experiment but never served: ${i.join(", ")}` : ""}. A row assigned to an arm the experiment does not declare is EXCLUDED, so the mismatch shows up as an empty arm rather than an error.`
249
+ });
250
+ }
251
+ }
252
+ return r;
253
+ }, V = (e, t) => {
254
+ let n = B(e, t);
255
+ if (n.length === 0) return;
256
+ let r = n.map((e) => ` · flag "${e.flag}" → experiment "${e.experiment}": ${e.problem}`);
257
+ throw Error(`@voltro/plugin-flags: ${n.length} flag→experiment link problem(s):\n${r.join("\n")}`);
258
+ }, H = 864e5, U = (e) => typeof e == "number" ? e : Date.parse(e), W = (e, t) => {
259
+ let n = x(e);
260
+ if (n.enabled === !1) return {
261
+ shape: "constantOff",
262
+ reason: "enabled: false — resolves false for every caller"
263
+ };
264
+ if (n.schedule) {
265
+ let { activateAt: e, deactivateAt: r } = n.schedule;
266
+ if (r != null && t >= U(r)) return {
267
+ shape: "expired",
268
+ reason: `schedule.deactivateAt (${String(r)}) has passed — the flag can never be on again`
269
+ };
270
+ if (e != null && t < U(e)) return {
271
+ shape: "notYetActive",
272
+ reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
273
+ };
274
+ }
275
+ let r = n.rollout ?? 100;
276
+ if (r <= 0) return {
277
+ shape: "constantOff",
278
+ reason: `rollout: ${r} — nobody is in the rollout`
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 < U(n.schedule.deactivateAt));
281
+ return r >= 100 && !i && !a && !o ? {
282
+ shape: "constantOn",
283
+ reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
284
+ } : {
285
+ shape: "conditional",
286
+ reason: `still selects between callers (${[
287
+ r < 100 ? `rollout ${r}%` : void 0,
288
+ i ? `${n.targeting?.length} targeting rule(s)` : void 0,
289
+ a ? `${n.variants?.length} variants` : void 0,
290
+ o ? "a live schedule" : void 0
291
+ ].filter((e) => e !== void 0).join(", ")})`
292
+ };
293
+ }, G = [
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
+ "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
+ "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
+ "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
+ "\"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
+ ], K = (e) => {
300
+ let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n * H, i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o;
301
+ for (let t of e.usage) {
302
+ let e = t.lastAt.getTime();
303
+ if (!Number.isFinite(e)) continue;
304
+ (o === void 0 || e < o) && (o = e);
305
+ let n = t.source === "bulk" ? a : i, r = n.get(t.flag);
306
+ (r === void 0 || e > r) && n.set(t.flag, e);
307
+ }
308
+ let s = o === void 0 ? 0 : Math.floor((t - o) / H);
309
+ return {
310
+ coverage: {
311
+ tracking: e.tracking,
312
+ observedSince: o === void 0 ? null : new Date(o).toISOString().slice(0, 10),
313
+ observedDays: s,
314
+ retentionDays: e.retentionDays,
315
+ staleAfterDays: n
316
+ },
317
+ findings: Object.entries(e.flags).map(([o, c]) => {
318
+ let { shape: l, reason: u } = W(c, t), d = i.get(o), f = a.get(o), p = [u], m;
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
+ let h = l === "constantOn" || l === "constantOff" || l === "expired";
321
+ return h && p.push("the definition alone makes this flag a constant — it selects nothing"), {
322
+ key: o,
323
+ shape: l,
324
+ shapeReason: u,
325
+ usage: m,
326
+ lastTargetedAt: d === void 0 ? null : new Date(d).toISOString(),
327
+ lastBulkAt: f === void 0 ? null : new Date(f).toISOString(),
328
+ liveTargetedEvaluations: e.liveCounts?.get(o) ?? 0,
329
+ removalCandidate: h || m === "stale",
330
+ why: p
331
+ };
332
+ }),
333
+ limits: G
334
+ };
335
+ }, q = "_voltro_feature_flag_usage", J = v(q, {
336
+ id: f({ prefix: "flagusg" }),
337
+ flag: y(),
338
+ day: y(),
339
+ source: y(),
340
+ lastAt: b()
341
+ }).unique([
342
+ "flag",
343
+ "day",
344
+ "source"
345
+ ]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]), ce = (e) => e.toISOString().slice(0, 10), le = (e) => {
346
+ let t = 2166136261;
347
+ for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
348
+ return (t >>> 0).toString(16).padStart(8, "0");
349
+ }, ue = (e, t, n) => `flagusg_${t.replace(/-/g, "")}_${n === "targeted" ? "t" : "b"}_${le(e)}`, Y = new class {
350
+ on = !1;
351
+ pending = /* @__PURE__ */ new Map();
352
+ liveCounts = /* @__PURE__ */ new Map();
353
+ enable() {
354
+ this.on = !0;
355
+ }
356
+ disable() {
357
+ this.on = !1, this.pending.clear();
358
+ }
359
+ get enabled() {
360
+ return this.on;
361
+ }
362
+ record(e, t, n = Date.now()) {
363
+ if (!this.on) return;
364
+ let r = `${t}\u0000${e}`, i = this.pending.get(r);
365
+ i ? i.lastAt = n : this.pending.set(r, {
366
+ flag: e,
367
+ source: t,
368
+ lastAt: n
369
+ }), t === "targeted" && this.liveCounts.set(e, (this.liveCounts.get(e) ?? 0) + 1);
370
+ }
371
+ drain() {
372
+ let e = [...this.pending.values()];
373
+ return this.pending.clear(), e;
374
+ }
375
+ restore(e) {
376
+ if (this.on) for (let t of e) {
377
+ let e = `${t.source}\u0000${t.flag}`, n = this.pending.get(e);
378
+ (!n || n.lastAt < t.lastAt) && this.pending.set(e, { ...t });
379
+ }
380
+ }
381
+ liveTargetedCounts() {
382
+ return this.liveCounts;
383
+ }
384
+ reset() {
385
+ this.on = !1, this.pending.clear(), this.liveCounts.clear();
386
+ }
387
+ }(), X = (e, t, n) => Y.record(e, t, n), de = (e) => e === "bulk" ? "bulk" : "targeted", fe = (e) => ({
388
+ upsertMany: async (t) => {
389
+ for (let n of t) {
390
+ let t = ce(n.lastAt);
391
+ await e.upsert(q, {
392
+ id: ue(n.flag, t, n.source),
393
+ flag: n.flag,
394
+ day: t,
395
+ source: n.source,
396
+ lastAt: n.lastAt
397
+ }, { conflictColumns: [
398
+ "flag",
399
+ "day",
400
+ "source"
401
+ ] });
402
+ }
403
+ },
404
+ load: async () => (await e.query(h(J).descriptor)).map((e) => ({
405
+ flag: String(e.flag),
406
+ day: String(e.day),
407
+ source: de(e.source),
408
+ lastAt: e.lastAt instanceof Date ? e.lastAt : new Date(String(e.lastAt))
409
+ }))
410
+ }), pe = 3e5, me = (e) => {
411
+ let t = e.intervalMs ?? 3e5, n = !1, r = async () => {
412
+ let t = Y.drain();
413
+ if (t.length !== 0) try {
414
+ await e.store.upsertMany(t.map((e) => ({
415
+ flag: e.flag,
416
+ source: e.source,
417
+ lastAt: new Date(e.lastAt)
418
+ })));
419
+ } catch (n) {
420
+ Y.restore(t), e.log?.warn?.("flag usage flush failed — observations retained for the next pass", {
421
+ pending: t.length,
422
+ cause: n.message
423
+ });
424
+ }
425
+ }, i = setInterval(() => {
426
+ r();
427
+ }, t);
428
+ return typeof i.unref == "function" && i.unref(), {
429
+ flushNow: r,
430
+ stop: async () => {
431
+ n || (n = !0, clearInterval(i), await r());
432
+ }
433
+ };
434
+ }, he = (e = process.env, t = 90) => {
435
+ let n = _(e.VOLTRO_FLAG_USAGE_TTL_HOURS, 24 * t);
436
+ return g({
437
+ table: q,
438
+ timeColumn: "lastAt",
439
+ source: "plugin",
440
+ ttlMs: n,
441
+ envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
442
+ }), n;
443
+ }, Z = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, Q = (e, t) => {
444
+ let n = I.get(t);
445
+ return X(t, "targeted"), n !== void 0 && O(t, n, Z(e));
446
+ }, ge = (e, t) => Q(e, t) ? c.void : c.fail(new s({ flag: t })), _e = (e, t) => {
447
+ let n = I.get(t.key);
448
+ return X(t.key, "targeted"), n === void 0 ? t.defaultValue : r(t, A(t.key, n, Z(e)));
449
+ }, ve = (e, t) => {
450
+ let n = I.get(t.key);
451
+ return X(t.key, "targeted"), n === void 0 ? null : A(t.key, n, Z(e)).name;
452
+ }, ye = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), $ = "@voltro/plugin-flags", be = (r = {}) => {
453
+ let a = r.gatedBy ?? {}, o = u({
454
+ base: $,
455
+ alias: r.alias,
456
+ instance: r.name
457
+ }), d = r.typedFlags ?? [], f = /* @__PURE__ */ new Map();
458
+ for (let e of d) {
459
+ if (f.has(e.key)) throw Error(`${$}: two typedFlags declare the key ${JSON.stringify(e.key)}`);
460
+ if (r.flags?.[e.key] !== void 0) throw Error(`${$}: ${JSON.stringify(e.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(e.key, e);
462
+ }
463
+ d.some((e) => e.experiment !== void 0) && V(d, r.experiments ?? []);
464
+ let p = { ...r.flags ?? {} };
465
+ for (let e of d) p[e.key] = e.definition;
466
+ let m = r.store === "postgres", h, g, _, v, y = [], b = r.usage ?? {}, x = b.track ?? m;
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
+ let S = b.staleAfterDays ?? 30, C = b.retentionDays ?? 90;
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 ee = (e) => {
471
+ let t = F(p, e), n = [];
472
+ for (let [e, r] of f) {
473
+ let a = t[e];
474
+ if (a === void 0) continue;
475
+ let o = i(r, typeof a == "boolean" ? { enabled: a } : a);
476
+ o.ok || (n.push(o.refusal), t[e] = r.definition);
477
+ }
478
+ y = n, I.set(t);
479
+ }, w = async () => {
480
+ h && ee(await h.loadOverrides());
481
+ }, T = (e) => {
482
+ if (a[e] !== void 0) return a[e];
483
+ for (let [t, n] of Object.entries(a)) if (t !== e && ye(t, e)) return n;
484
+ }, E = (e, t) => {
485
+ let n = T(t.tag);
486
+ return n === void 0 || Q({ subject: t.subject }, n) ? e : c.fail(new s({ flag: n }));
487
+ }, D = Object.keys(a).length > 0, O = (e) => {
488
+ for (let t of e) X(t, "bulk");
489
+ }, te = {
226
490
  ...e,
227
491
  description: "Resolve every feature flag for the calling subject.",
228
- execute: (e, t) => i.succeed(E(N.all(), t.request.subject ?? {}))
229
- }, v = {
492
+ execute: (e, t) => c.sync(() => {
493
+ let e = I.all();
494
+ return O(Object.keys(e)), ne(e, t.request.subject ?? {});
495
+ })
496
+ }, k = {
230
497
  ...t,
231
498
  description: "Resolve every feature flag to its served variant for the calling subject.",
232
- execute: (e, t) => i.succeed(D(N.all(), t.request.subject ?? {}))
233
- }, y = (e) => ({
499
+ execute: (e, t) => c.sync(() => {
500
+ let e = I.all();
501
+ return O(Object.keys(e)), re(e, t.request.subject ?? {});
502
+ })
503
+ }, A = (e) => ({
234
504
  kind: "json",
235
505
  data: e
236
- }), b = (e, t) => ({
506
+ }), j = (e, t) => ({
237
507
  kind: "json",
238
508
  status: e,
239
509
  data: { error: t }
240
- }), x = [
510
+ }), M = async () => {
511
+ let e = [];
512
+ if (x && _) try {
513
+ e = await _.load();
514
+ } catch {
515
+ e = [];
516
+ }
517
+ return K({
518
+ flags: I.all(),
519
+ usage: e,
520
+ tracking: x,
521
+ liveCounts: Y.liveTargetedCounts(),
522
+ staleAfterDays: S,
523
+ retentionDays: C
524
+ });
525
+ }, L = () => {
526
+ let e = {};
527
+ for (let [t, n] of f) e[t] = {
528
+ valueType: n.valueType,
529
+ variants: n.variants.map((e) => e.name),
530
+ experiment: n.experiment ?? null
531
+ };
532
+ return e;
533
+ }, oe = [
241
534
  {
242
535
  method: "GET",
243
536
  path: "/list",
244
- description: "Every resolved flag (key, enabled, rollout, description).",
245
- handler: () => i.succeed(y({ flags: I(N.all()) }))
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: () => c.promise(async () => {
539
+ let e = await M();
540
+ return A({
541
+ flags: z(I.all()),
542
+ lifecycle: e,
543
+ typed: L(),
544
+ overrideRefusals: y
545
+ });
546
+ })
246
547
  },
247
548
  {
248
549
  method: "POST",
249
550
  path: "/toggle",
250
551
  description: "Flip a flag's kill-switch on/off (live registry; durable on the postgres tier). Logs an audit row.",
251
- handler: (e) => i.gen(function* () {
552
+ handler: (e) => c.gen(function* () {
252
553
  let t;
253
554
  try {
254
555
  t = JSON.parse(e.body || "{}");
255
556
  } catch {
256
- return b(400, "invalid JSON body");
557
+ return j(400, "invalid JSON body");
257
558
  }
258
- if (!t.key || typeof t.enabled != "boolean") return b(400, "key (string) + enabled (boolean) required");
259
- let n = F(t.key, t.enabled);
260
- if (n === null) return b(404, `unknown flag "${t.key}"`);
261
- let r = d ? N.get(t.key) : void 0;
262
- return d && r !== void 0 && (yield* i.promise(() => d.upsertOverride(L(t.key, r)))), f && (yield* i.promise(() => f.record({
559
+ if (!t.key || typeof t.enabled != "boolean") return j(400, "key (string) + enabled (boolean) required");
560
+ let n = R(t.key, t.enabled);
561
+ if (n === null) return j(404, `unknown flag "${t.key}"`);
562
+ let r = h ? I.get(t.key) : void 0;
563
+ return h && r !== void 0 && (yield* c.promise(() => h.upsertOverride(ie(t.key, r)))), g && (yield* c.promise(() => g.record({
263
564
  flag: t.key,
264
565
  enabled: t.enabled,
265
566
  previousEnabled: n,
266
567
  actorId: typeof t.actor == "string" ? t.actor : null,
267
568
  at: /* @__PURE__ */ new Date()
268
- }))), y({
569
+ }))), A({
269
570
  ok: !0,
270
- flags: I(N.all())
571
+ flags: z(I.all())
271
572
  });
272
573
  })
273
574
  },
@@ -275,16 +576,16 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
275
576
  method: "GET",
276
577
  path: "/audit",
277
578
  description: "The kill-switch flip audit trail, newest-first. `?flag=<key>` filters; `?limit=<n>` caps (postgres tier only).",
278
- handler: (e) => i.gen(function* () {
279
- if (!f) return y({
579
+ handler: (e) => c.gen(function* () {
580
+ if (!g) return A({
280
581
  audit: [],
281
582
  note: "audit trail requires store: \"postgres\""
282
583
  });
283
- let t = new URL(e.url, "http://local"), n = t.searchParams.get("flag"), r = t.searchParams.get("limit"), a = r != null && Number.isFinite(Number(r)) ? Number(r) : 100, o = yield* i.promise(() => f.list({
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* c.promise(() => g.list({
284
585
  ...n == null ? {} : { flag: n },
285
- limit: a
586
+ limit: i
286
587
  }));
287
- return y({ audit: o.map((e) => ({
588
+ return A({ audit: a.map((e) => ({
288
589
  flag: e.flag,
289
590
  enabled: e.enabled,
290
591
  previousEnabled: e.previousEnabled,
@@ -294,48 +595,68 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
294
595
  })
295
596
  }
296
597
  ];
297
- return a({
298
- name: l,
598
+ return l({
599
+ name: o,
600
+ baseName: $,
299
601
  description: "Feature flags — targeting, % rollout, kill-switch, declarative gating + client evaluation.",
300
602
  permissions: [
301
- ...g ? [
603
+ ...D ? [
302
604
  "rpc:intercept:mutation",
303
605
  "rpc:intercept:query",
304
606
  "rpc:intercept:action"
305
607
  ] : [],
306
- ...u ? ["store:write", "store:changes:read"] : [],
608
+ ...m ? ["store:write", "store:changes:read"] : [],
307
609
  "inspect:read",
308
610
  "inspect:write"
309
611
  ],
310
- routes: [_, v],
612
+ routes: [te, k],
311
613
  rpcClientDescriptors: n,
312
- inspectEndpoints: x,
614
+ inspectEndpoints: oe,
313
615
  errorSchemas: [{
314
- schema: r,
616
+ schema: s,
315
617
  import: {
316
618
  module: "@voltro/plugin-flags/errors",
317
619
  name: "FlagDisabled"
318
620
  }
319
621
  }],
320
- ...g ? {
321
- interceptMutation: h,
322
- interceptQuery: h,
323
- interceptAction: h
622
+ ...D ? {
623
+ interceptMutation: E,
624
+ interceptQuery: E,
625
+ interceptAction: E
324
626
  } : {},
325
- ...u ? { extendSchema: { tables: [A, j] } } : {},
326
- ...u ? { bindDataStore: (e) => {
327
- d = R(e), f = B(e);
627
+ ...m ? { extendSchema: { tables: x ? [
628
+ N,
629
+ P,
630
+ J
631
+ ] : [N, P] } } : {},
632
+ ...m ? { bindDataStore: (e) => {
633
+ h = ae(e), g = se(e), x && (_ = fe(e));
328
634
  } } : {},
329
- ...u ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? i.tryPromise(() => p()) : i.void } : {},
330
- onActivate: (e) => i.gen(function* () {
331
- N.set(s), u && d && (yield* i.promise(p)), e.logger.info("feature flags active", {
332
- count: Object.keys(s).length,
333
- gated: Object.keys(c).length,
334
- store: o.store ?? "memory"
635
+ ...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? c.tryPromise(() => w()) : c.void } : {},
636
+ onActivate: (e) => c.gen(function* () {
637
+ I.set(p), m && h && (yield* c.promise(w));
638
+ let t;
639
+ x && _ && (t = he(process.env, C), Y.enable(), v = me({
640
+ store: _,
641
+ intervalMs: b.flushIntervalMs ?? 3e5,
642
+ log: { warn: (t, n) => e.logger.warn(t, n) }
643
+ }));
644
+ for (let t of y) e.logger.warn("feature flag override REFUSED — the code-declared definition stands", {
645
+ flag: t.key,
646
+ reason: t.reason
647
+ });
648
+ e.logger.info("feature flags active", {
649
+ count: Object.keys(p).length,
650
+ typed: f.size,
651
+ gated: Object.keys(a).length,
652
+ store: r.store ?? "memory",
653
+ usage: x ? `tracking (stale after ${S}d, observations kept ${Math.round((t ?? 0) / 864e5)}d)` : "off — no dead-flag evidence is collected"
335
654
  });
336
655
  }),
337
- onDeactivate: () => i.sync(() => N.set({}))
656
+ onDeactivate: () => c.promise(async () => {
657
+ await v?.stop(), v = void 0, Y.disable(), I.set({});
658
+ })
338
659
  });
339
660
  };
340
661
  //#endregion
341
- export { r as FlagDisabled, I as describeFlags, E as evaluateAll, D as evaluateAllVariants, S as evaluateFlag, x as evaluateSchedule, T as evaluateVariant, A as featureFlagsTable, j as flagAuditTable, N as flagRegistry, G as flagsPlugin, F as flipFlagEnabled, H as isFlagEnabled, M as mergeOverrides, U as requireFlag, w as resolveVariant, P as setFlagEnabled, m as toDefinition };
662
+ export { pe as DEFAULT_FLAG_USAGE_FLUSH_MS, G as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, s as FlagDisabled, V as assertFlagExperimentLinks, B as checkFlagExperimentLinks, W as classifyShape, fe as dataStoreFlagUsageStore, o as defineFlag, z as describeFlags, ne as evaluateAll, re as evaluateAllVariants, O as evaluateFlag, D as evaluateSchedule, A as evaluateVariant, N as featureFlagsTable, P as flagAuditTable, K as flagLifecycleReport, I as flagRegistry, Y as flagUsageBuffer, ue as flagUsageRowId, J as flagUsageTable, _e as flagValue, ve as flagVariant, be as flagsPlugin, R as flipFlagEnabled, i as gateOverride, Q as isFlagEnabled, a as isTypedFlag, F as mergeOverrides, X as recordFlagEvaluation, he as registerFlagUsageRetention, ge as requireFlag, k as resolveVariant, L as setFlagEnabled, me as startFlagUsageFlush, x as toDefinition, r as typedValueOf, ce as utcDay };