@davidlee49/launchgate 0.0.0-stage → 0.5.1

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.
@@ -0,0 +1,161 @@
1
+ /** What a flag can resolve to. Boolean is the common case, not the only one (Decision 1). */
2
+ type FlagValue = boolean | string | number;
3
+ /**
4
+ * A flag's retirement — the terminal state of its lifecycle (Decision 11).
5
+ *
6
+ * Presence is the retirement: a def carrying this resolves to `value` and **no
7
+ * source is consulted**, not the env kill switch and not a signed override
8
+ * cookie. That ordering is the point. A retirement is a decision someone
9
+ * committed to git, so the only way to reverse it is to revert the commit —
10
+ * never a leftover `FLAG_X=on` secret or a cookie an engineer is still holding.
11
+ *
12
+ * The entry **stays in the registry** after retirement, which is Unleash's
13
+ * archive-rather-than-delete rule and buys the same property: the key cannot be
14
+ * silently reused by a new feature (a duplicate key in the registry literal is
15
+ * a type error), so a stale override can never attach to something it was never
16
+ * about. Delete the entry only once its code is gone.
17
+ */
18
+ interface Retirement<T extends FlagValue = FlagValue> {
19
+ /**
20
+ * When retirement was decided, `YYYY-MM-DD`. Required, because the clock is
21
+ * the whole point: retired code that nobody deletes is the flag debt this
22
+ * state exists to make visible. `auditRegistry` warns once it is older than
23
+ * the budget.
24
+ */
25
+ since: string;
26
+ /**
27
+ * What every evaluation returns from now on — and the *disposition record*:
28
+ * Unleash makes you declare an outcome when completing a flag, because the
29
+ * cleanup PR has to know which branch of the code survives. `false` retires a
30
+ * feature outright; `"v1"` on a dynamic-config flag says v1 is what the code
31
+ * keeps.
32
+ */
33
+ value: T;
34
+ }
35
+ interface FlagDef<T extends FlagValue = FlagValue> {
36
+ /**
37
+ * The value used when no source decides, and when a source throws.
38
+ *
39
+ * Per flag, never global (Decision 3): a work-in-progress gate's safe state is
40
+ * `false`, but a kill switch on a year-old feature's safe state is `true` —
41
+ * failing that one closed would take down working functionality because a
42
+ * database blipped.
43
+ */
44
+ fallback: T;
45
+ /** Why this flag exists. Greppability plus this line is the whole flag-debt mitigation. */
46
+ description: string;
47
+ /** May a signed visitor cookie force this flag? Default false — see `cookieOverride`. */
48
+ overridable?: boolean;
49
+ /** Per-environment value, consulted by the `envDefault` source. */
50
+ byEnv?: Record<string, T>;
51
+ /**
52
+ * Retired: resolve to `retired.value` and consult no source at all. See
53
+ * `Retirement` — this outranks every source, including the kill switch.
54
+ */
55
+ retired?: Retirement<T>;
56
+ /**
57
+ * Projects may hang their own fields here — a stage, a tier, an owner — and
58
+ * read them back inside a custom source. The registry keeps their real types
59
+ * (`flags.network.stage` is a `string`); only the erased view a `Source` sees
60
+ * widens to `unknown`.
61
+ */
62
+ [projectField: string]: unknown;
63
+ }
64
+ type FlagRegistry = Record<string, FlagDef>;
65
+ /**
66
+ * The type a flag resolves to.
67
+ *
68
+ * `fallback: false` infers as the *literal* `false` rather than `boolean`,
69
+ * because `boolean` is itself `true | false` and TypeScript preserves a literal
70
+ * whose contextual type contains it. Left alone, every boolean flag would be
71
+ * unable to hold the opposite value. Strings and numbers do not have this
72
+ * problem (their contextual types are not literal unions), so they keep whatever
73
+ * the author declared — which is what makes `fallback: "v1" as "v1" | "v2"` work.
74
+ */
75
+ type FlagValueOf<T extends FlagRegistry, K extends keyof T> = T[K]["fallback"] extends boolean ? boolean : T[K]["fallback"];
76
+ /**
77
+ * Data a source may target on. Named for OpenFeature's evaluation context, and
78
+ * `targetingKey` is its field — the CNCF spec every major SDK implements, so
79
+ * this reads as a familiar dialect rather than a private vocabulary.
80
+ *
81
+ * `targetingKey` is **optional** by design, where OpenFeature makes it merely
82
+ * conventional: an anonymous visitor to a static site has none, and a launch
83
+ * gate must still resolve for them.
84
+ */
85
+ interface EvaluationContext {
86
+ /** Who is being evaluated — a user id, org id, device id. Absent for anonymous visitors. */
87
+ targetingKey?: string;
88
+ /** Reads a request cookie. Supplied by a framework adapter; absent means "no cookies here". */
89
+ cookie?: (name: string) => string | undefined;
90
+ /** Anything else a project-supplied source needs. */
91
+ [key: string]: unknown;
92
+ }
93
+ /** @deprecated Renamed to `EvaluationContext` in 0.3.0 to match OpenFeature. */
94
+ type Context = EvaluationContext;
95
+ /** A source's answer. `undefined` means *undecided — ask the next source* (Decision 2). */
96
+ type Decision = FlagValue | undefined;
97
+ interface FlagMeta {
98
+ key: string;
99
+ def: FlagDef;
100
+ }
101
+ /**
102
+ * One link in the resolution chain. Sources are typed against `unknown` values
103
+ * rather than the registry's inferred types (Decision 8) — the call site keeps
104
+ * its types, the four built-in sources give theirs up.
105
+ */
106
+ interface Source {
107
+ /** Reported to `onError` when this source throws. */
108
+ readonly name: string;
109
+ decide(flag: FlagMeta, ctx: Context): Decision | Promise<Decision>;
110
+ }
111
+ interface SourceErrorInfo {
112
+ flag: string;
113
+ source: string;
114
+ }
115
+
116
+ /**
117
+ * Declares a project's flags. Identity at runtime; it exists to infer each flag's
118
+ * value type so `resolve()` is typed at the call site.
119
+ *
120
+ * The registry lives in the project that reads the flags, not in a table and not
121
+ * in library config (Decision 4): flags ship with the code they gate, so deleting
122
+ * one is a pull request and a key nobody reads is visibly dead.
123
+ */
124
+ declare function defineFlags<T extends FlagRegistry>(flags: T): T;
125
+ interface ResolverOptions<T extends FlagRegistry> {
126
+ flags: T;
127
+ /**
128
+ * Consulted in order; the first definite answer wins. Recommended ordering:
129
+ * `envOverride` (kill switch), `cookieOverride`, project sources (grants,
130
+ * opt-in, entitlement), `envDefault`. The flag's `fallback` is terminal and is
131
+ * not a source, so it cannot be left out.
132
+ */
133
+ sources: Source[];
134
+ /** Called when a source throws or answers with the wrong type. */
135
+ onError?: (error: unknown, info: SourceErrorInfo) => void;
136
+ }
137
+ /** Every flag in a registry, evaluated. */
138
+ type ResolvedFlags<T extends FlagRegistry> = {
139
+ [K in keyof T]: FlagValueOf<T, K>;
140
+ };
141
+ interface Resolver<T extends FlagRegistry> {
142
+ resolve<K extends keyof T & string>(key: K, ctx?: EvaluationContext): Promise<FlagValueOf<T, K>>;
143
+ /**
144
+ * Evaluate the whole registry at once — LaunchDarkly's `allFlagsState()`
145
+ * pattern: resolve server-side, hand the map to the client, and the UI never
146
+ * flickers because it never had to ask.
147
+ *
148
+ * Resolution is concurrent and shares the context object, so subject-backed
149
+ * sources hit their per-request cache; with `subjectStore({ loadAll })` the
150
+ * whole registry costs one read.
151
+ *
152
+ * Pass the **values** to a client, never the registry — flag keys name
153
+ * unshipped work, and a viewer who can't see a surface shouldn't learn its
154
+ * name from the payload.
155
+ */
156
+ resolveAll(ctx?: EvaluationContext): Promise<ResolvedFlags<T>>;
157
+ readonly flags: T;
158
+ }
159
+ declare function createResolver<T extends FlagRegistry>(options: ResolverOptions<T>): Resolver<T>;
160
+
161
+ export { type Context as C, type Decision as D, type EvaluationContext as E, type FlagValue as F, type Resolver as R, type Source as S, type FlagMeta as a, type FlagRegistry as b, type FlagValueOf as c, type FlagDef as d, type ResolvedFlags as e, type ResolverOptions as f, type Retirement as g, type SourceErrorInfo as h, createResolver as i, defineFlags as j };
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
- {
2
- "name": "@davidlee49/launchgate",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@davidlee49/launchgate",
3
+ "version": "0.5.1",
4
+ "description": "A launch-gate library: hide work in progress, kill a shipped feature, no platform required.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "API.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./next": {
17
+ "types": "./dist/next/index.d.ts",
18
+ "import": "./dist/next/index.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "check": "tsc --noEmit && vitest run && tsup",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "prepublishOnly": "pnpm check"
27
+ },
28
+ "devDependencies": {
29
+ "tsup": "^8.5.0",
30
+ "typescript": "^5.9.2",
31
+ "vitest": "^3.2.4",
32
+ "next": "^16.0.0",
33
+ "react": "^19.0.0",
34
+ "react-dom": "^19.0.0",
35
+ "@types/react": "^19.0.0"
36
+ },
37
+ "packageManager": "pnpm@10.29.2",
38
+ "peerDependencies": {
39
+ "next": ">=15"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "next": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/davidlee49/launchgate.git"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }