@ultimat3/flags 30.9.0 → 31.0.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/CLAUDE.md +8 -7
- package/README.md +11 -3
- package/package.json +2 -2
- package/src/index.ts +8 -27
- package/src/runtime.ts +27 -23
package/CLAUDE.md
CHANGED
|
@@ -113,13 +113,14 @@ what lets `policy` (tier 2) call it from inside a predicate.
|
|
|
113
113
|
- **`configureFlags` screens `reportEveryMs` with `finiteCount`, before it touches anything.**
|
|
114
114
|
`now - previous < NaN` is false, so a `NaN` interval reported on every evaluation; `Infinity`
|
|
115
115
|
muted the flag for good. `0` is legal — a caller may choose it.
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
- **The clock is core's `ambientClock()`, never this package's** (v31: `configureFlags({ clock })`
|
|
117
|
+
is gone; `clock` in `apps/<app>/runtime.ts`, or `installClock` in a test). **`flagsClock()` clears
|
|
118
|
+
the report watermarks when the ambient clock is a different object from the one they were read
|
|
119
|
+
on, and only then.** A monotonic reading is meaningful only against the clock that produced it: a
|
|
120
|
+
process that reported at monotonic 10,000,000 and then took a clock starting at 0 computed
|
|
121
|
+
`now - previous` as -10,000,000, below every interval — on a frozen test clock, never again.
|
|
121
122
|
An interval change re-reads the SAME clock and must NOT clear, or the rate limit becomes a
|
|
122
|
-
suggestion one configure call can bypass.
|
|
123
|
+
suggestion one configure call can bypass (`runtime.test.ts`).
|
|
123
124
|
|
|
124
125
|
## Open
|
|
125
126
|
|
|
@@ -134,6 +135,6 @@ what lets `policy` (tier 2) call it from inside a predicate.
|
|
|
134
135
|
## Commands
|
|
135
136
|
|
|
136
137
|
```
|
|
137
|
-
bun test packages/flags
|
|
138
|
+
bun test ./packages/flags/
|
|
138
139
|
bun run --filter @ultimat3/flags typecheck
|
|
139
140
|
```
|
package/README.md
CHANGED
|
@@ -165,16 +165,24 @@ work on the day it is needed.
|
|
|
165
165
|
|
|
166
166
|
There is no reporter seam in this package. An overdue flag goes through `@ultimat3/core`'s
|
|
167
167
|
`ErrorReporter` — the framework's one error-monitoring seam — as a `warning` from `source:
|
|
168
|
-
'process'`, so an app wires its monitor in exactly one place
|
|
168
|
+
'process'`, so an app wires its monitor in exactly one place, `apps/web/runtime.ts` (or sets
|
|
169
|
+
`SENTRY_DSN` and lets the boot add the transport):
|
|
169
170
|
|
|
170
171
|
```ts
|
|
171
|
-
|
|
172
|
+
import type { RuntimeOverrides } from '@ultimat3/cli/serve';
|
|
173
|
+
import { sentryErrorReporter } from '@ultimat3/core';
|
|
174
|
+
|
|
175
|
+
declare const dsn: string;
|
|
176
|
+
|
|
177
|
+
export const runtime = { errorReporter: sentryErrorReporter({ dsn }) } satisfies RuntimeOverrides;
|
|
172
178
|
```
|
|
173
179
|
|
|
174
180
|
What this package adds is the rate limit core has no opinion about: one report per flag per
|
|
175
181
|
`DEFAULT_REPORT_INTERVAL_MS` (1 hour), on the **monotonic** clock, so a flag read on every request
|
|
176
182
|
does not become the loudest thing in the monitor — which is how a report that fires per call ends
|
|
177
|
-
up muted and the debt invisible again. `configureFlags({
|
|
183
|
+
up muted and the debt invisible again. `configureFlags({ reportEveryMs })` tunes it — the clock is
|
|
184
|
+
`@ultimat3/core`'s process clock (`ambientClock()`), set by `clock` in `apps/<app>/runtime.ts`, so
|
|
185
|
+
`configureFlags` takes none since 31.0.0;
|
|
178
186
|
`reportEveryMs` is a whole number of milliseconds, 0 or more — `NaN`, `Infinity`, a fraction or a
|
|
179
187
|
negative is refused (`X_INVARIANT`) and changes nothing.
|
|
180
188
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/flags",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "31.0.0",
|
|
4
4
|
"description": "Feature flags: permanent switches, and temporary ones that cannot be forgotten",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ultimate",
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"test": "bun test"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@ultimat3/core": "
|
|
44
|
+
"@ultimat3/core": "31.0.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
// Public API of @ultimat3/flags. Explicit re-exports only.
|
|
2
2
|
|
|
3
|
-
export { BUCKETS
|
|
4
|
-
export type {
|
|
5
|
-
export {
|
|
6
|
-
FLAGS_ERROR_CODES,
|
|
7
|
-
FLAGS_ERROR_TITLES,
|
|
8
|
-
FlagsError,
|
|
9
|
-
flagDuplicate,
|
|
10
|
-
flagExpired,
|
|
11
|
-
flagExpiryInvalid,
|
|
12
|
-
flagSubjectRequired,
|
|
13
|
-
flagTargetingInvalid,
|
|
14
|
-
flagUnknown,
|
|
15
|
-
} from './errors';
|
|
3
|
+
export { BUCKETS } from './bucket';
|
|
4
|
+
export type { FlagsErrorCode } from './errors';
|
|
5
|
+
export { FlagsError, flagExpiryInvalid } from './errors';
|
|
16
6
|
export { isEnabled } from './evaluate';
|
|
17
|
-
export type {
|
|
18
|
-
Flag,
|
|
19
|
-
FlagDef,
|
|
20
|
-
FlagExpiryIsMandatory,
|
|
21
|
-
FlagKind,
|
|
22
|
-
PermanentFlagDef,
|
|
23
|
-
TemporaryFlagDef,
|
|
24
|
-
} from './flag';
|
|
25
|
-
export { FLAG_KINDS } from './flag';
|
|
7
|
+
export type { Flag, FlagDef, FlagKind, PermanentFlagDef, TemporaryFlagDef } from './flag';
|
|
26
8
|
export type { FlagFacts, FlagsReport } from './projection';
|
|
27
9
|
export { flagsReport } from './projection';
|
|
28
10
|
export type { SnapshotResult } from './registry';
|
|
29
|
-
export {
|
|
11
|
+
export { applyFlagSnapshot, defineFlag } from './registry';
|
|
30
12
|
export type { FlagsRuntimeOptions } from './runtime';
|
|
31
13
|
// The reporter seam is `@ultimat3/core`'s `ErrorReporter`, wired once with
|
|
32
|
-
// `
|
|
33
|
-
export { configureFlags
|
|
34
|
-
export type {
|
|
35
|
-
export { BUILT_IN_SUBJECT_KINDS } from './subject';
|
|
14
|
+
// `runtime.ts`'s `errorReporter`. This package deliberately re-exports none of it.
|
|
15
|
+
export { configureFlags } from './runtime';
|
|
16
|
+
export type { FlagSubjects } from './subject';
|
|
36
17
|
export type { FlagTargeting } from './targeting';
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Single responsibility: the
|
|
2
|
-
//
|
|
1
|
+
// Single responsibility: how often the same overdue flag may report, and the watermarks that rate
|
|
2
|
+
// limit keeps. "Now" is not this package's: it is core's `ambientClock()`, which the boot fills
|
|
3
|
+
// from `clock` in `apps/<app>/runtime.ts` and a test from `@ultimat3/testing` (v31).
|
|
3
4
|
//
|
|
4
5
|
// The reporter itself is NOT here. `@ultimat3/core`'s `ErrorReporter` is the framework's one
|
|
5
6
|
// error-monitoring seam; a second one in this package would be a second place an app has to wire
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
// caught failure, while a flag is evaluated on every request.
|
|
9
10
|
|
|
10
11
|
import type { Clock, UltimateError } from '@ultimat3/core';
|
|
11
|
-
import { finiteCount, recordFailureFact, reportError
|
|
12
|
+
import { ambientClock, finiteCount, recordFailureFact, reportError } from '@ultimat3/core';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* One hour. Small enough that an overdue flag shows up the same day, large enough that a flag read
|
|
@@ -18,27 +19,36 @@ import { finiteCount, recordFailureFact, reportError, systemClock } from '@ultim
|
|
|
18
19
|
export const DEFAULT_REPORT_INTERVAL_MS = 60 * 60 * 1000;
|
|
19
20
|
|
|
20
21
|
export interface FlagsRuntimeOptions {
|
|
21
|
-
readonly clock?: Clock | undefined;
|
|
22
22
|
/** Minimum gap between two reports of the SAME flag key. */
|
|
23
23
|
readonly reportEveryMs?: number | undefined;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
let clock: Clock = systemClock;
|
|
27
26
|
let reportEveryMs = DEFAULT_REPORT_INTERVAL_MS;
|
|
28
27
|
const lastReportedAt = new Map<string, number>();
|
|
28
|
+
/** The clock every watermark in `lastReportedAt` was read from. */
|
|
29
|
+
let watermarkClock: Clock | undefined;
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* test kits actually call.
|
|
32
|
+
* The process clock, and the watermarks forgotten whenever it is a DIFFERENT clock from the one
|
|
33
|
+
* they were read on: a monotonic reading is only meaningful against the clock that produced it. A
|
|
34
|
+
* process that reported at monotonic 10_000_000 and then took a clock starting at 0 computed
|
|
35
|
+
* `now - previous` as -10_000_000 — below every interval, so that key could never report again
|
|
36
|
+
* until the new clock passed the old one's reading, which on a frozen test clock is never.
|
|
37
37
|
*
|
|
38
|
-
* Only
|
|
39
|
-
* through on every configure call and turn the rate limit into a suggestion.
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
* Only on a clock change. An interval change re-reads the SAME clock, so clearing there would let
|
|
39
|
+
* a report through on every configure call and turn the rate limit into a suggestion.
|
|
40
|
+
*/
|
|
41
|
+
export const flagsClock = (): Clock => {
|
|
42
|
+
const clock = ambientClock();
|
|
43
|
+
if (clock !== watermarkClock) {
|
|
44
|
+
lastReportedAt.clear();
|
|
45
|
+
watermarkClock = clock;
|
|
46
|
+
}
|
|
47
|
+
return clock;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `reportEveryMs` is screened, and BEFORE anything is touched so a refused call changes nothing.
|
|
42
52
|
* `now - previous < NaN` is false, so a `NaN` interval — `Number(process.env.X)` on an unset
|
|
43
53
|
* variable — removed the rate limit entirely and an overdue flag reported on every evaluation;
|
|
44
54
|
* `Infinity` did the opposite and muted it for good. `0` stays legal: every evaluation reporting
|
|
@@ -49,15 +59,9 @@ export function configureFlags(options: FlagsRuntimeOptions): void {
|
|
|
49
59
|
options.reportEveryMs === undefined
|
|
50
60
|
? reportEveryMs
|
|
51
61
|
: finiteCount('configureFlags', 'reportEveryMs', options.reportEveryMs);
|
|
52
|
-
if (options.clock !== undefined && options.clock !== clock) {
|
|
53
|
-
clock = options.clock;
|
|
54
|
-
lastReportedAt.clear();
|
|
55
|
-
}
|
|
56
62
|
reportEveryMs = interval;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
export const flagsClock = (): Clock => clock;
|
|
60
|
-
|
|
61
65
|
/**
|
|
62
66
|
* Report `build()`'s error through core's seam at most once per `reportEveryMs` for this key, and
|
|
63
67
|
* say whether it did. The rate limit is keyed on the MONOTONIC clock: a wall-clock jump — an NTP
|
|
@@ -71,7 +75,7 @@ export const flagsClock = (): Clock => clock;
|
|
|
71
75
|
* happened to evaluate it first.
|
|
72
76
|
*/
|
|
73
77
|
export function reportOnce(key: string, build: () => UltimateError): boolean {
|
|
74
|
-
const now =
|
|
78
|
+
const now = flagsClock().monotonic();
|
|
75
79
|
const previous = lastReportedAt.get(key);
|
|
76
80
|
if (previous !== undefined && now - previous < reportEveryMs) return false;
|
|
77
81
|
lastReportedAt.set(key, now);
|
|
@@ -92,7 +96,7 @@ export function recordEvaluation(key: string, on: boolean): boolean {
|
|
|
92
96
|
|
|
93
97
|
/** Test-only, and the counterpart to every other reset in the framework. */
|
|
94
98
|
export function resetFlagReporting(): void {
|
|
95
|
-
|
|
99
|
+
watermarkClock = undefined;
|
|
96
100
|
reportEveryMs = DEFAULT_REPORT_INTERVAL_MS;
|
|
97
101
|
lastReportedAt.clear();
|
|
98
102
|
}
|