@velajs/feature-flags 0.1.0 → 0.1.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.
- package/CHANGELOG.md +6 -0
- package/dist/feature-flags.service-CNOZ1DhZ.js +343 -0
- package/dist/feature-flags.service-CNOZ1DhZ.js.map +1 -0
- package/dist/index.d.ts +115 -12
- package/dist/index.js +114 -15
- package/dist/index.js.map +1 -0
- package/dist/memory.driver-C-5Y2b7_.d.ts +235 -0
- package/dist/testing/index.d.ts +11 -11
- package/dist/testing/index.js +28 -34
- package/dist/testing/index.js.map +1 -0
- package/package.json +53 -39
- package/dist/decorators/feature-flag.decorator.d.ts +0 -35
- package/dist/decorators/feature-flag.decorator.js +0 -26
- package/dist/drivers/driver.d.ts +0 -21
- package/dist/drivers/driver.js +0 -13
- package/dist/drivers/memory.driver.d.ts +0 -35
- package/dist/drivers/memory.driver.js +0 -50
- package/dist/drivers/registry.d.ts +0 -24
- package/dist/drivers/registry.js +0 -51
- package/dist/feature-flags.error.d.ts +0 -14
- package/dist/feature-flags.error.js +0 -15
- package/dist/feature-flags.module.d.ts +0 -21
- package/dist/feature-flags.module.js +0 -63
- package/dist/feature-flags.service.d.ts +0 -83
- package/dist/feature-flags.service.js +0 -208
- package/dist/feature-flags.tokens.d.ts +0 -16
- package/dist/feature-flags.tokens.js +0 -10
- package/dist/feature-flags.types.d.ts +0 -71
- package/dist/feature-flags.types.js +0 -3
- package/dist/guards/feature-flag.guard.d.ts +0 -24
- package/dist/guards/feature-flag.guard.js +0 -52
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { Inject, Injectable, Logger, Optional, Scope, getCurrentRequestContext, moduleToken } from "@velajs/vela";
|
|
2
|
+
//#region src/feature-flags.error.ts
|
|
3
|
+
/**
|
|
4
|
+
* The single error type for `@velajs/feature-flags`.
|
|
5
|
+
*
|
|
6
|
+
* Note: flag *evaluation* never throws — the service absorbs driver failures
|
|
7
|
+
* into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
|
|
8
|
+
* is reserved for *configuration* faults surfaced at wiring time: an unknown
|
|
9
|
+
* driver name, a duplicate driver, or an empty driver set.
|
|
10
|
+
*/
|
|
11
|
+
var FeatureFlagError = class extends Error {
|
|
12
|
+
name = "FeatureFlagError";
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/drivers/memory.driver.ts
|
|
19
|
+
/**
|
|
20
|
+
* In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
|
|
21
|
+
* and the test fake in one. Reads from an internal `Map` seeded from options
|
|
22
|
+
* or mutated with {@link set}/{@link reset} — the memory driver ignores the
|
|
23
|
+
* evaluation context (it does no targeting). An unknown key returns the
|
|
24
|
+
* caller's `fallback`, exactly like a remote driver that can't resolve it.
|
|
25
|
+
*/
|
|
26
|
+
var MemoryFlagDriver = class {
|
|
27
|
+
name;
|
|
28
|
+
store;
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.name = options.name ?? "memory";
|
|
31
|
+
this.store = new Map(Object.entries(options.values ?? {}));
|
|
32
|
+
}
|
|
33
|
+
/** Set a flag value. Chainable. */
|
|
34
|
+
set(key, value) {
|
|
35
|
+
this.store.set(key, value);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/** Remove a flag (subsequent reads return the caller's fallback). Chainable. */
|
|
39
|
+
delete(key) {
|
|
40
|
+
this.store.delete(key);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
/** True when `key` has a stored value. */
|
|
44
|
+
has(key) {
|
|
45
|
+
return this.store.has(key);
|
|
46
|
+
}
|
|
47
|
+
/** Clear all flags, then optionally seed a fresh set. Chainable. */
|
|
48
|
+
reset(values) {
|
|
49
|
+
this.store.clear();
|
|
50
|
+
if (values) for (const [key, value] of Object.entries(values)) this.store.set(key, value);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
getBoolean(key, fallback, _ctx) {
|
|
54
|
+
return Promise.resolve(this.read(key, fallback));
|
|
55
|
+
}
|
|
56
|
+
getString(key, fallback, _ctx) {
|
|
57
|
+
return Promise.resolve(this.read(key, fallback));
|
|
58
|
+
}
|
|
59
|
+
getNumber(key, fallback, _ctx) {
|
|
60
|
+
return Promise.resolve(this.read(key, fallback));
|
|
61
|
+
}
|
|
62
|
+
getObject(key, fallback, _ctx) {
|
|
63
|
+
return Promise.resolve(this.read(key, fallback));
|
|
64
|
+
}
|
|
65
|
+
read(key, fallback) {
|
|
66
|
+
return this.store.has(key) ? this.store.get(key) : fallback;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/** Convenience factory for {@link MemoryFlagDriver}. */
|
|
70
|
+
function memoryFlagDriver(options) {
|
|
71
|
+
return new MemoryFlagDriver(options);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/drivers/registry.ts
|
|
75
|
+
/**
|
|
76
|
+
* The set of registered {@link FeatureFlagDriver}s plus the default selection.
|
|
77
|
+
* Built once per module instance ({@link buildDriverRegistry}) and injected
|
|
78
|
+
* into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
|
|
79
|
+
*/
|
|
80
|
+
var FeatureFlagDriverRegistry = class {
|
|
81
|
+
drivers = /* @__PURE__ */ new Map();
|
|
82
|
+
defaultName;
|
|
83
|
+
constructor(drivers, defaultName) {
|
|
84
|
+
if (drivers.length === 0) throw new FeatureFlagError("No feature flag drivers registered. Provide at least one driver to FeatureFlagsModule.forRoot({ drivers: [...] }).");
|
|
85
|
+
for (const driver of drivers) {
|
|
86
|
+
if (this.drivers.has(driver.name)) throw new FeatureFlagError(`Duplicate feature flag driver "${driver.name}".`);
|
|
87
|
+
this.drivers.set(driver.name, driver);
|
|
88
|
+
}
|
|
89
|
+
const requested = defaultName ?? drivers[0].name;
|
|
90
|
+
if (!this.drivers.has(requested)) throw new FeatureFlagError(`Default feature flag driver "${requested}" is not registered.`);
|
|
91
|
+
this.defaultName = requested;
|
|
92
|
+
}
|
|
93
|
+
/** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */
|
|
94
|
+
get(name) {
|
|
95
|
+
const driver = this.drivers.get(name);
|
|
96
|
+
if (!driver) throw new FeatureFlagError(`Feature flag driver "${name}" is not registered.`);
|
|
97
|
+
return driver;
|
|
98
|
+
}
|
|
99
|
+
/** Resolve a named driver, or the default when `name` is omitted. */
|
|
100
|
+
resolve(name) {
|
|
101
|
+
return this.get(name ?? this.defaultName);
|
|
102
|
+
}
|
|
103
|
+
/** All registered driver names. */
|
|
104
|
+
names() {
|
|
105
|
+
return [...this.drivers.keys()];
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Build the driver registry from module options. Falls back to a single
|
|
110
|
+
* in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
|
|
111
|
+
* yields a working (all-defaults) flags service for local development.
|
|
112
|
+
*/
|
|
113
|
+
function buildDriverRegistry(options) {
|
|
114
|
+
return new FeatureFlagDriverRegistry(options.drivers && options.drivers.length > 0 ? options.drivers : [new MemoryFlagDriver()], options.default);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/feature-flags.tokens.ts
|
|
118
|
+
/**
|
|
119
|
+
* Injection tokens for the feature-flags module. Named on the
|
|
120
|
+
* `vela:feature-flags:<thing>` convention so identity is stable across
|
|
121
|
+
* refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.
|
|
122
|
+
*/
|
|
123
|
+
const FEATURE_FLAG_TOKENS = {
|
|
124
|
+
/** The resolved {@link FeatureFlagsOptions} (module options token). */
|
|
125
|
+
Options: moduleToken("vela:feature-flags:options"),
|
|
126
|
+
/** The injectable {@link FeatureFlagsService}. */
|
|
127
|
+
Service: moduleToken("vela:feature-flags:service"),
|
|
128
|
+
/** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */
|
|
129
|
+
DriverRegistry: moduleToken("vela:feature-flags:driver-registry")
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateMetadata.js
|
|
133
|
+
function __decorateMetadata(k, v) {
|
|
134
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateParam.js
|
|
138
|
+
function __decorateParam(paramIndex, decorator) {
|
|
139
|
+
return function(target, key) {
|
|
140
|
+
decorator(target, key, paramIndex);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
|
|
145
|
+
function __decorate(decorators, target, key, desc) {
|
|
146
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
147
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
148
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
149
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/feature-flags.service.ts
|
|
153
|
+
var _FeatureFlagsService;
|
|
154
|
+
/**
|
|
155
|
+
* Safely read the current request context. Returns `undefined` outside a
|
|
156
|
+
* request or when ambient access isn't enabled — never throws — so the service
|
|
157
|
+
* resolves and evaluates in queue / scheduled / global scope too.
|
|
158
|
+
*/
|
|
159
|
+
function ambientRequestContext() {
|
|
160
|
+
try {
|
|
161
|
+
return getCurrentRequestContext();
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let FeatureFlagsService = _FeatureFlagsService = class FeatureFlagsService {
|
|
167
|
+
registry;
|
|
168
|
+
options;
|
|
169
|
+
boundContext;
|
|
170
|
+
logger;
|
|
171
|
+
manifest;
|
|
172
|
+
driver;
|
|
173
|
+
constructor(registry, options, logger, boundContext, boundDriver) {
|
|
174
|
+
this.registry = registry;
|
|
175
|
+
this.options = options;
|
|
176
|
+
this.boundContext = boundContext;
|
|
177
|
+
this.logger = logger ?? new Logger("FeatureFlags");
|
|
178
|
+
this.manifest = options.manifest ?? {};
|
|
179
|
+
this.driver = boundDriver ?? registry.resolve(options.default);
|
|
180
|
+
}
|
|
181
|
+
/** The name of the driver this instance targets. */
|
|
182
|
+
get driverName() {
|
|
183
|
+
return this.driver.name;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Switch to a different registered driver. Returns a NEW immutable instance
|
|
187
|
+
* bound to `name`; the original is unchanged. Throws if `name` is unknown.
|
|
188
|
+
*/
|
|
189
|
+
use(name) {
|
|
190
|
+
if (name === this.driver.name) return this;
|
|
191
|
+
return this.clone({ driver: this.registry.get(name) });
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Bind a request context. Returns a NEW immutable instance whose evaluations
|
|
195
|
+
* merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
|
|
196
|
+
* resolved in request scope can also call it explicitly.
|
|
197
|
+
*/
|
|
198
|
+
forRequest(ctx) {
|
|
199
|
+
return this.clone({ context: ctx });
|
|
200
|
+
}
|
|
201
|
+
/** Evaluate a flag as a `boolean`. */
|
|
202
|
+
async getBooleanValue(flagKey, defaultValue, context) {
|
|
203
|
+
const fallback = this.fallback(flagKey, defaultValue, false);
|
|
204
|
+
return this.safe(flagKey, async () => this.driver.getBoolean(flagKey, fallback, await this.context(context)), () => fallback);
|
|
205
|
+
}
|
|
206
|
+
/** Evaluate a flag as a `string`. */
|
|
207
|
+
async getStringValue(flagKey, defaultValue, context) {
|
|
208
|
+
const fallback = this.fallback(flagKey, defaultValue, "");
|
|
209
|
+
return this.safe(flagKey, async () => this.driver.getString(flagKey, fallback, await this.context(context)), () => fallback);
|
|
210
|
+
}
|
|
211
|
+
/** Evaluate a flag as a `number`. */
|
|
212
|
+
async getNumberValue(flagKey, defaultValue, context) {
|
|
213
|
+
const fallback = this.fallback(flagKey, defaultValue, 0);
|
|
214
|
+
return this.safe(flagKey, async () => this.driver.getNumber(flagKey, fallback, await this.context(context)), () => fallback);
|
|
215
|
+
}
|
|
216
|
+
/** Evaluate a flag as a typed object. */
|
|
217
|
+
async getObjectValue(flagKey, defaultValue, context) {
|
|
218
|
+
const fallback = this.fallback(flagKey, defaultValue, {});
|
|
219
|
+
return this.safe(flagKey, async () => this.driver.getObject(flagKey, fallback, await this.context(context)), () => fallback);
|
|
220
|
+
}
|
|
221
|
+
/** Evaluate a `boolean` flag with synthesized evaluation metadata. */
|
|
222
|
+
async getBooleanDetails(flagKey, defaultValue, context) {
|
|
223
|
+
const fallback = this.fallback(flagKey, defaultValue, false);
|
|
224
|
+
return this.safe(flagKey, async () => this.details(flagKey, await this.driver.getBoolean(flagKey, fallback, await this.context(context))), (error) => this.errorDetails(flagKey, fallback, error));
|
|
225
|
+
}
|
|
226
|
+
/** Evaluate a `string` flag with synthesized evaluation metadata. */
|
|
227
|
+
async getStringDetails(flagKey, defaultValue, context) {
|
|
228
|
+
const fallback = this.fallback(flagKey, defaultValue, "");
|
|
229
|
+
return this.safe(flagKey, async () => this.details(flagKey, await this.driver.getString(flagKey, fallback, await this.context(context))), (error) => this.errorDetails(flagKey, fallback, error));
|
|
230
|
+
}
|
|
231
|
+
/** Evaluate a `number` flag with synthesized evaluation metadata. */
|
|
232
|
+
async getNumberDetails(flagKey, defaultValue, context) {
|
|
233
|
+
const fallback = this.fallback(flagKey, defaultValue, 0);
|
|
234
|
+
return this.safe(flagKey, async () => this.details(flagKey, await this.driver.getNumber(flagKey, fallback, await this.context(context))), (error) => this.errorDetails(flagKey, fallback, error));
|
|
235
|
+
}
|
|
236
|
+
/** Evaluate a typed object flag with synthesized evaluation metadata. */
|
|
237
|
+
async getObjectDetails(flagKey, defaultValue, context) {
|
|
238
|
+
const fallback = this.fallback(flagKey, defaultValue, {});
|
|
239
|
+
return this.safe(flagKey, async () => this.details(flagKey, await this.driver.getObject(flagKey, fallback, await this.context(context))), (error) => this.errorDetails(flagKey, fallback, error));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Evaluate every flag declared in the manifest and return a `{ key: value }`
|
|
243
|
+
* map. The evaluation method is chosen from each declared default's type.
|
|
244
|
+
* A throwing context resolver falls back to the manifest defaults rather than
|
|
245
|
+
* taking the batch down.
|
|
246
|
+
*/
|
|
247
|
+
async all(context) {
|
|
248
|
+
const keys = Object.keys(this.manifest);
|
|
249
|
+
let merged;
|
|
250
|
+
try {
|
|
251
|
+
merged = await this.context(context);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.logger.warn(`Feature flag context resolution failed on driver "${this.driver.name}"; returning manifest defaults.`, { error: message(error) });
|
|
254
|
+
return { ...this.manifest };
|
|
255
|
+
}
|
|
256
|
+
const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)));
|
|
257
|
+
const result = {};
|
|
258
|
+
keys.forEach((key, i) => {
|
|
259
|
+
result[key] = values[i];
|
|
260
|
+
});
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
/** Immutable clone with a different driver and/or bound context. */
|
|
264
|
+
clone(overrides) {
|
|
265
|
+
return new _FeatureFlagsService(this.registry, this.options, this.logger, overrides.context ?? this.boundContext, overrides.driver ?? this.driver);
|
|
266
|
+
}
|
|
267
|
+
/** Resolve the merged evaluation context (default context + per-call override). */
|
|
268
|
+
async context(callContext) {
|
|
269
|
+
const base = this.boundContext ?? ambientRequestContext();
|
|
270
|
+
if (!this.options.context || !base) return callContext;
|
|
271
|
+
const resolved = await this.options.context(base);
|
|
272
|
+
return callContext ? {
|
|
273
|
+
...resolved,
|
|
274
|
+
...callContext
|
|
275
|
+
} : resolved;
|
|
276
|
+
}
|
|
277
|
+
/** Pick the default: explicit arg, then manifest, then the type's zero value. */
|
|
278
|
+
fallback(flagKey, provided, zero) {
|
|
279
|
+
if (provided !== void 0) return provided;
|
|
280
|
+
if (flagKey in this.manifest) return this.manifest[flagKey];
|
|
281
|
+
return zero;
|
|
282
|
+
}
|
|
283
|
+
/** Evaluate a single flag, choosing the method from the declared default's type. */
|
|
284
|
+
evaluate(flagKey, declared, context) {
|
|
285
|
+
switch (typeof declared) {
|
|
286
|
+
case "boolean": return this.safe(flagKey, () => this.driver.getBoolean(flagKey, declared, context), () => declared);
|
|
287
|
+
case "number": return this.safe(flagKey, () => this.driver.getNumber(flagKey, declared, context), () => declared);
|
|
288
|
+
case "string": return this.safe(flagKey, () => this.driver.getString(flagKey, declared, context), () => declared);
|
|
289
|
+
default: return this.safe(flagKey, () => this.driver.getObject(flagKey, declared, context), () => declared);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Run an evaluation and absorb any failure into the fallback. A flag lookup
|
|
294
|
+
* must never take the caller down with it.
|
|
295
|
+
*/
|
|
296
|
+
async safe(flagKey, evaluate, onError) {
|
|
297
|
+
try {
|
|
298
|
+
return await evaluate();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.logger.warn(`Feature flag evaluation failed for "${flagKey}" on driver "${this.driver.name}"; returning the fallback value.`, { error: message(error) });
|
|
301
|
+
return onError(error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
details(flagKey, value) {
|
|
305
|
+
return {
|
|
306
|
+
flagKey,
|
|
307
|
+
value,
|
|
308
|
+
reason: "STATIC"
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
errorDetails(flagKey, value, error) {
|
|
312
|
+
return {
|
|
313
|
+
flagKey,
|
|
314
|
+
value,
|
|
315
|
+
reason: "ERROR",
|
|
316
|
+
errorMessage: message(error)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
FeatureFlagsService = _FeatureFlagsService = __decorate([
|
|
321
|
+
Injectable({ scope: Scope.TRANSIENT }),
|
|
322
|
+
__decorateParam(0, Inject(FEATURE_FLAG_TOKENS.DriverRegistry)),
|
|
323
|
+
__decorateParam(1, Inject(FEATURE_FLAG_TOKENS.Options)),
|
|
324
|
+
__decorateParam(2, Optional()),
|
|
325
|
+
__decorateParam(2, Inject(Logger)),
|
|
326
|
+
__decorateParam(3, Optional()),
|
|
327
|
+
__decorateParam(4, Optional()),
|
|
328
|
+
__decorateMetadata("design:paramtypes", [
|
|
329
|
+
Object,
|
|
330
|
+
Object,
|
|
331
|
+
Object,
|
|
332
|
+
Object,
|
|
333
|
+
Object
|
|
334
|
+
])
|
|
335
|
+
], FeatureFlagsService);
|
|
336
|
+
/** Extract a human-readable message from an unknown thrown value. */
|
|
337
|
+
function message(error) {
|
|
338
|
+
return error instanceof Error ? error.message : String(error);
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
export { FEATURE_FLAG_TOKENS as a, MemoryFlagDriver as c, __decorateMetadata as i, memoryFlagDriver as l, __decorate as n, FeatureFlagDriverRegistry as o, __decorateParam as r, buildDriverRegistry as s, FeatureFlagsService as t, FeatureFlagError as u };
|
|
342
|
+
|
|
343
|
+
//# sourceMappingURL=feature-flags.service-CNOZ1DhZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flags.service-CNOZ1DhZ.js","names":[],"sources":["../src/feature-flags.error.ts","../src/drivers/memory.driver.ts","../src/drivers/registry.ts","../src/feature-flags.tokens.ts","../src/feature-flags.service.ts"],"sourcesContent":["/**\n * The single error type for `@velajs/feature-flags`.\n *\n * Note: flag *evaluation* never throws — the service absorbs driver failures\n * into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`\n * is reserved for *configuration* faults surfaced at wiring time: an unknown\n * driver name, a duplicate driver, or an empty driver set.\n */\nexport class FeatureFlagError extends Error {\n override name = 'FeatureFlagError';\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n }\n}\n","import type { FeatureFlagDriver } from './driver';\nimport type { FlagContext, FlagManifest, FlagValue } from '../feature-flags.types';\n\nexport interface MemoryFlagDriverOptions {\n /** Driver name used for `use(name)` / the default-driver selection. Default `\"memory\"`. */\n name?: string;\n /** Initial flag values. */\n values?: FlagManifest;\n}\n\n/**\n * In-memory {@link FeatureFlagDriver}: the default driver shipped in-package\n * and the test fake in one. Reads from an internal `Map` seeded from options\n * or mutated with {@link set}/{@link reset} — the memory driver ignores the\n * evaluation context (it does no targeting). An unknown key returns the\n * caller's `fallback`, exactly like a remote driver that can't resolve it.\n */\nexport class MemoryFlagDriver implements FeatureFlagDriver {\n readonly name: string;\n private readonly store: Map<string, FlagValue>;\n\n constructor(options: MemoryFlagDriverOptions = {}) {\n this.name = options.name ?? 'memory';\n this.store = new Map(Object.entries(options.values ?? {}));\n }\n\n /** Set a flag value. Chainable. */\n set(key: string, value: FlagValue): this {\n this.store.set(key, value);\n return this;\n }\n\n /** Remove a flag (subsequent reads return the caller's fallback). Chainable. */\n delete(key: string): this {\n this.store.delete(key);\n return this;\n }\n\n /** True when `key` has a stored value. */\n has(key: string): boolean {\n return this.store.has(key);\n }\n\n /** Clear all flags, then optionally seed a fresh set. Chainable. */\n reset(values?: FlagManifest): this {\n this.store.clear();\n if (values) {\n for (const [key, value] of Object.entries(values)) this.store.set(key, value);\n }\n return this;\n }\n\n getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean> {\n return Promise.resolve(this.read(key, fallback));\n }\n\n getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string> {\n return Promise.resolve(this.read(key, fallback));\n }\n\n getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number> {\n return Promise.resolve(this.read(key, fallback));\n }\n\n getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T> {\n return Promise.resolve(this.read(key, fallback));\n }\n\n private read<T extends FlagValue>(key: string, fallback: T): T {\n return this.store.has(key) ? (this.store.get(key) as T) : fallback;\n }\n}\n\n/** Convenience factory for {@link MemoryFlagDriver}. */\nexport function memoryFlagDriver(options?: MemoryFlagDriverOptions): MemoryFlagDriver {\n return new MemoryFlagDriver(options);\n}\n","import { FeatureFlagError } from '../feature-flags.error';\nimport type { FeatureFlagsOptions } from '../feature-flags.types';\nimport type { FeatureFlagDriver } from './driver';\nimport { MemoryFlagDriver } from './memory.driver';\n\n/**\n * The set of registered {@link FeatureFlagDriver}s plus the default selection.\n * Built once per module instance ({@link buildDriverRegistry}) and injected\n * into {@link FeatureFlagsService}; `use(name)` looks a driver up here.\n */\nexport class FeatureFlagDriverRegistry {\n private readonly drivers = new Map<string, FeatureFlagDriver>();\n readonly defaultName: string;\n\n constructor(drivers: readonly FeatureFlagDriver[], defaultName?: string) {\n if (drivers.length === 0) {\n throw new FeatureFlagError(\n 'No feature flag drivers registered. Provide at least one driver to FeatureFlagsModule.forRoot({ drivers: [...] }).',\n );\n }\n for (const driver of drivers) {\n if (this.drivers.has(driver.name)) {\n throw new FeatureFlagError(`Duplicate feature flag driver \"${driver.name}\".`);\n }\n this.drivers.set(driver.name, driver);\n }\n const requested = defaultName ?? drivers[0]!.name;\n if (!this.drivers.has(requested)) {\n throw new FeatureFlagError(`Default feature flag driver \"${requested}\" is not registered.`);\n }\n this.defaultName = requested;\n }\n\n /** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */\n get(name: string): FeatureFlagDriver {\n const driver = this.drivers.get(name);\n if (!driver) {\n throw new FeatureFlagError(`Feature flag driver \"${name}\" is not registered.`);\n }\n return driver;\n }\n\n /** Resolve a named driver, or the default when `name` is omitted. */\n resolve(name?: string): FeatureFlagDriver {\n return this.get(name ?? this.defaultName);\n }\n\n /** All registered driver names. */\n names(): string[] {\n return [...this.drivers.keys()];\n }\n}\n\n/**\n * Build the driver registry from module options. Falls back to a single\n * in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`\n * yields a working (all-defaults) flags service for local development.\n */\nexport function buildDriverRegistry(options: FeatureFlagsOptions): FeatureFlagDriverRegistry {\n const drivers =\n options.drivers && options.drivers.length > 0 ? options.drivers : [new MemoryFlagDriver()];\n return new FeatureFlagDriverRegistry(drivers, options.default);\n}\n","import { moduleToken } from '@velajs/vela';\nimport type { FeatureFlagDriverRegistry } from './drivers/registry';\nimport type { FeatureFlagsService } from './feature-flags.service';\nimport type { FeatureFlagsOptions } from './feature-flags.types';\n\n/**\n * Injection tokens for the feature-flags module. Named on the\n * `vela:feature-flags:<thing>` convention so identity is stable across\n * refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.\n */\nexport const FEATURE_FLAG_TOKENS = {\n /** The resolved {@link FeatureFlagsOptions} (module options token). */\n Options: moduleToken<FeatureFlagsOptions>('vela:feature-flags:options'),\n /** The injectable {@link FeatureFlagsService}. */\n Service: moduleToken<FeatureFlagsService>('vela:feature-flags:service'),\n /** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */\n DriverRegistry: moduleToken<FeatureFlagDriverRegistry>('vela:feature-flags:driver-registry'),\n} as const;\n","// Never-throw + manifest-default ergonomics adapted from\n// @stratal/feature-flags (MIT, © Temitayo Fadojutimi), reshaped from a single\n// Cloudflare-binding service into this driver-based, edge-pure form.\nimport {\n Inject,\n Injectable,\n Logger,\n Optional,\n Scope,\n getCurrentRequestContext,\n type LoggerService,\n type RequestContext,\n} from '@velajs/vela';\nimport type { FeatureFlagDriver } from './drivers/driver';\nimport type { FeatureFlagDriverRegistry } from './drivers/registry';\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens';\nimport type {\n FeatureFlagsOptions,\n FlagContext,\n FlagEvaluationDetails,\n FlagKey,\n FlagManifest,\n FlagValue,\n} from './feature-flags.types';\n\n/**\n * Safely read the current request context. Returns `undefined` outside a\n * request or when ambient access isn't enabled — never throws — so the service\n * resolves and evaluates in queue / scheduled / global scope too.\n */\nfunction ambientRequestContext(): RequestContext | undefined {\n try {\n return getCurrentRequestContext();\n } catch {\n return undefined;\n }\n}\n\n/**\n * The injectable consumers reach for. A thin, type-safe, never-throw wrapper\n * over a {@link FeatureFlagDriver}, with two ergonomic additions:\n *\n * - **Manifest defaults** — omit a default and the value declared in the app's\n * `manifest` is used (an explicit argument always wins).\n * - **Default context** — the module's `context` resolver is merged into every\n * evaluation (per-call context overrides it), resolved from the current\n * request and skipped automatically outside request scope.\n *\n * Switch drivers with {@link use}; bind a request with {@link forRequest} (the\n * guard does this). Evaluation NEVER throws — the driver returns the fallback\n * on evaluation errors, and the service additionally absorbs any thrown error\n * into the same fallback with a logged warning.\n *\n * `@Transient`: constructed synchronously (lazy-module-safe), a fresh instance\n * per injection, and resolvable in and out of request scope — it never\n * DI-injects `REQUEST_CONTEXT`, so it does not bubble to request scope.\n */\n@Injectable({ scope: Scope.TRANSIENT })\nexport class FeatureFlagsService {\n private readonly logger: LoggerService;\n private readonly manifest: FlagManifest;\n private readonly driver: FeatureFlagDriver;\n\n constructor(\n @Inject(FEATURE_FLAG_TOKENS.DriverRegistry)\n private readonly registry: FeatureFlagDriverRegistry,\n @Inject(FEATURE_FLAG_TOKENS.Options) private readonly options: FeatureFlagsOptions,\n @Optional() @Inject(Logger) logger?: LoggerService,\n // The next two are never provided by DI (they carry `@Optional()` so the\n // container leaves them `undefined`). `forRequest()` and `use()` set them\n // when cloning; tests may pass them directly.\n @Optional() private readonly boundContext?: RequestContext,\n @Optional() boundDriver?: FeatureFlagDriver,\n ) {\n this.logger = logger ?? new Logger('FeatureFlags');\n this.manifest = options.manifest ?? {};\n this.driver = boundDriver ?? registry.resolve(options.default);\n }\n\n /** The name of the driver this instance targets. */\n get driverName(): string {\n return this.driver.name;\n }\n\n /**\n * Switch to a different registered driver. Returns a NEW immutable instance\n * bound to `name`; the original is unchanged. Throws if `name` is unknown.\n */\n use(name: string): FeatureFlagsService {\n if (name === this.driver.name) return this;\n return this.clone({ driver: this.registry.get(name) });\n }\n\n /**\n * Bind a request context. Returns a NEW immutable instance whose evaluations\n * merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers\n * resolved in request scope can also call it explicitly.\n */\n forRequest(ctx: RequestContext): FeatureFlagsService {\n return this.clone({ context: ctx });\n }\n\n // ==================== EVALUATION ====================\n\n /** Evaluate a flag as a `boolean`. */\n async getBooleanValue(\n flagKey: FlagKey,\n defaultValue?: boolean,\n context?: FlagContext,\n ): Promise<boolean> {\n const fallback = this.fallback(flagKey, defaultValue, false);\n return this.safe(\n flagKey,\n async () => this.driver.getBoolean(flagKey, fallback, await this.context(context)),\n () => fallback,\n );\n }\n\n /** Evaluate a flag as a `string`. */\n async getStringValue(\n flagKey: FlagKey,\n defaultValue?: string,\n context?: FlagContext,\n ): Promise<string> {\n const fallback = this.fallback(flagKey, defaultValue, '');\n return this.safe(\n flagKey,\n async () => this.driver.getString(flagKey, fallback, await this.context(context)),\n () => fallback,\n );\n }\n\n /** Evaluate a flag as a `number`. */\n async getNumberValue(\n flagKey: FlagKey,\n defaultValue?: number,\n context?: FlagContext,\n ): Promise<number> {\n const fallback = this.fallback(flagKey, defaultValue, 0);\n return this.safe(\n flagKey,\n async () => this.driver.getNumber(flagKey, fallback, await this.context(context)),\n () => fallback,\n );\n }\n\n /** Evaluate a flag as a typed object. */\n async getObjectValue<T extends object>(\n flagKey: FlagKey,\n defaultValue?: T,\n context?: FlagContext,\n ): Promise<T> {\n const fallback = this.fallback(flagKey, defaultValue, {} as T);\n return this.safe(\n flagKey,\n async () => this.driver.getObject<T>(flagKey, fallback, await this.context(context)),\n () => fallback,\n );\n }\n\n /** Evaluate a `boolean` flag with synthesized evaluation metadata. */\n async getBooleanDetails(\n flagKey: FlagKey,\n defaultValue?: boolean,\n context?: FlagContext,\n ): Promise<FlagEvaluationDetails<boolean>> {\n const fallback = this.fallback(flagKey, defaultValue, false);\n return this.safe(\n flagKey,\n async () =>\n this.details(\n flagKey,\n await this.driver.getBoolean(flagKey, fallback, await this.context(context)),\n ),\n (error) => this.errorDetails(flagKey, fallback, error),\n );\n }\n\n /** Evaluate a `string` flag with synthesized evaluation metadata. */\n async getStringDetails(\n flagKey: FlagKey,\n defaultValue?: string,\n context?: FlagContext,\n ): Promise<FlagEvaluationDetails<string>> {\n const fallback = this.fallback(flagKey, defaultValue, '');\n return this.safe(\n flagKey,\n async () =>\n this.details(\n flagKey,\n await this.driver.getString(flagKey, fallback, await this.context(context)),\n ),\n (error) => this.errorDetails(flagKey, fallback, error),\n );\n }\n\n /** Evaluate a `number` flag with synthesized evaluation metadata. */\n async getNumberDetails(\n flagKey: FlagKey,\n defaultValue?: number,\n context?: FlagContext,\n ): Promise<FlagEvaluationDetails<number>> {\n const fallback = this.fallback(flagKey, defaultValue, 0);\n return this.safe(\n flagKey,\n async () =>\n this.details(\n flagKey,\n await this.driver.getNumber(flagKey, fallback, await this.context(context)),\n ),\n (error) => this.errorDetails(flagKey, fallback, error),\n );\n }\n\n /** Evaluate a typed object flag with synthesized evaluation metadata. */\n async getObjectDetails<T extends object>(\n flagKey: FlagKey,\n defaultValue?: T,\n context?: FlagContext,\n ): Promise<FlagEvaluationDetails<T>> {\n const fallback = this.fallback(flagKey, defaultValue, {} as T);\n return this.safe(\n flagKey,\n async () =>\n this.details(\n flagKey,\n await this.driver.getObject<T>(flagKey, fallback, await this.context(context)),\n ),\n (error) => this.errorDetails(flagKey, fallback, error),\n );\n }\n\n /**\n * Evaluate every flag declared in the manifest and return a `{ key: value }`\n * map. The evaluation method is chosen from each declared default's type.\n * A throwing context resolver falls back to the manifest defaults rather than\n * taking the batch down.\n */\n async all(context?: FlagContext): Promise<Record<string, FlagValue>> {\n const keys = Object.keys(this.manifest);\n let merged: FlagContext | undefined;\n try {\n merged = await this.context(context);\n } catch (error) {\n this.logger.warn(\n `Feature flag context resolution failed on driver \"${this.driver.name}\"; returning manifest defaults.`,\n { error: message(error) },\n );\n return { ...this.manifest };\n }\n const values = await Promise.all(\n keys.map((key) => this.evaluate(key, this.manifest[key]!, merged)),\n );\n const result: Record<string, FlagValue> = {};\n keys.forEach((key, i) => {\n result[key] = values[i]!;\n });\n return result;\n }\n\n // ==================== INTERNAL ====================\n\n /** Immutable clone with a different driver and/or bound context. */\n private clone(overrides: {\n driver?: FeatureFlagDriver;\n context?: RequestContext;\n }): FeatureFlagsService {\n return new FeatureFlagsService(\n this.registry,\n this.options,\n this.logger,\n overrides.context ?? this.boundContext,\n overrides.driver ?? this.driver,\n );\n }\n\n /** Resolve the merged evaluation context (default context + per-call override). */\n private async context(callContext?: FlagContext): Promise<FlagContext | undefined> {\n const base = this.boundContext ?? ambientRequestContext();\n if (!this.options.context || !base) return callContext;\n const resolved = await this.options.context(base);\n return callContext ? { ...resolved, ...callContext } : resolved;\n }\n\n /** Pick the default: explicit arg, then manifest, then the type's zero value. */\n private fallback<T extends FlagValue>(flagKey: string, provided: T | undefined, zero: T): T {\n if (provided !== undefined) return provided;\n if (flagKey in this.manifest) return this.manifest[flagKey] as T;\n return zero;\n }\n\n /** Evaluate a single flag, choosing the method from the declared default's type. */\n private evaluate(\n flagKey: string,\n declared: FlagValue,\n context?: FlagContext,\n ): Promise<FlagValue> {\n switch (typeof declared) {\n case 'boolean':\n return this.safe(\n flagKey,\n () => this.driver.getBoolean(flagKey, declared, context),\n () => declared,\n );\n case 'number':\n return this.safe(\n flagKey,\n () => this.driver.getNumber(flagKey, declared, context),\n () => declared,\n );\n case 'string':\n return this.safe(\n flagKey,\n () => this.driver.getString(flagKey, declared, context),\n () => declared,\n );\n default:\n return this.safe(\n flagKey,\n () => this.driver.getObject(flagKey, declared as object, context),\n () => declared,\n );\n }\n }\n\n /**\n * Run an evaluation and absorb any failure into the fallback. A flag lookup\n * must never take the caller down with it.\n */\n private async safe<T>(\n flagKey: string,\n evaluate: () => Promise<T>,\n onError: (error: unknown) => T,\n ): Promise<T> {\n try {\n return await evaluate();\n } catch (error) {\n this.logger.warn(\n `Feature flag evaluation failed for \"${flagKey}\" on driver \"${this.driver.name}\"; returning the fallback value.`,\n { error: message(error) },\n );\n return onError(error);\n }\n }\n\n private details<T extends FlagValue>(flagKey: string, value: T): FlagEvaluationDetails<T> {\n return { flagKey, value, reason: 'STATIC' };\n }\n\n private errorDetails<T extends FlagValue>(\n flagKey: string,\n value: T,\n error: unknown,\n ): FlagEvaluationDetails<T> {\n return { flagKey, value, reason: 'ERROR', errorMessage: message(error) };\n }\n}\n\n/** Extract a human-readable message from an unknown thrown value. */\nfunction message(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;;;;;;;;AAQA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,OAAgB;CAEhB,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,SAAS,UAAU,KAAA,IAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,KAAA,CAAS;CACpF;AACF;;;;;;;;;;ACGA,IAAa,mBAAb,MAA2D;CACzD;CACA;CAEA,YAAY,UAAmC,CAAC,GAAG;EACjD,KAAK,OAAO,QAAQ,QAAQ;EAC5B,KAAK,QAAQ,IAAI,IAAI,OAAO,QAAQ,QAAQ,UAAU,CAAC,CAAC,CAAC;CAC3D;;CAGA,IAAI,KAAa,OAAwB;EACvC,KAAK,MAAM,IAAI,KAAK,KAAK;EACzB,OAAO;CACT;;CAGA,OAAO,KAAmB;EACxB,KAAK,MAAM,OAAO,GAAG;EACrB,OAAO;CACT;;CAGA,IAAI,KAAsB;EACxB,OAAO,KAAK,MAAM,IAAI,GAAG;CAC3B;;CAGA,MAAM,QAA6B;EACjC,KAAK,MAAM,MAAM;EACjB,IAAI,QACF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK;EAE9E,OAAO;CACT;CAEA,WAAW,KAAa,UAAmB,MAAsC;EAC/E,OAAO,QAAQ,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;CACjD;CAEA,UAAU,KAAa,UAAkB,MAAqC;EAC5E,OAAO,QAAQ,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;CACjD;CAEA,UAAU,KAAa,UAAkB,MAAqC;EAC5E,OAAO,QAAQ,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;CACjD;CAEA,UAA4B,KAAa,UAAa,MAAgC;EACpF,OAAO,QAAQ,QAAQ,KAAK,KAAK,KAAK,QAAQ,CAAC;CACjD;CAEA,KAAkC,KAAa,UAAgB;EAC7D,OAAO,KAAK,MAAM,IAAI,GAAG,IAAK,KAAK,MAAM,IAAI,GAAG,IAAU;CAC5D;AACF;;AAGA,SAAgB,iBAAiB,SAAqD;CACpF,OAAO,IAAI,iBAAiB,OAAO;AACrC;;;;;;;;AClEA,IAAa,4BAAb,MAAuC;CACrC,0BAA2B,IAAI,IAA+B;CAC9D;CAEA,YAAY,SAAuC,aAAsB;EACvE,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,iBACR,oHACF;EAEF,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,GAC9B,MAAM,IAAI,iBAAiB,kCAAkC,OAAO,KAAK,GAAG;GAE9E,KAAK,QAAQ,IAAI,OAAO,MAAM,MAAM;EACtC;EACA,MAAM,YAAY,eAAe,QAAQ,EAAE,CAAE;EAC7C,IAAI,CAAC,KAAK,QAAQ,IAAI,SAAS,GAC7B,MAAM,IAAI,iBAAiB,gCAAgC,UAAU,qBAAqB;EAE5F,KAAK,cAAc;CACrB;;CAGA,IAAI,MAAiC;EACnC,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI;EACpC,IAAI,CAAC,QACH,MAAM,IAAI,iBAAiB,wBAAwB,KAAK,qBAAqB;EAE/E,OAAO;CACT;;CAGA,QAAQ,MAAkC;EACxC,OAAO,KAAK,IAAI,QAAQ,KAAK,WAAW;CAC1C;;CAGA,QAAkB;EAChB,OAAO,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;CAChC;AACF;;;;;;AAOA,SAAgB,oBAAoB,SAAyD;CAG3F,OAAO,IAAI,0BADT,QAAQ,WAAW,QAAQ,QAAQ,SAAS,IAAI,QAAQ,UAAU,CAAC,IAAI,iBAAiB,CAAC,GAC7C,QAAQ,OAAO;AAC/D;;;;;;;;ACpDA,MAAa,sBAAsB;;CAEjC,SAAS,YAAiC,4BAA4B;;CAEtE,SAAS,YAAiC,4BAA4B;;CAEtE,gBAAgB,YAAuC,oCAAoC;AAC7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACaA,SAAS,wBAAoD;CAC3D,IAAI;EACF,OAAO,yBAAyB;CAClC,QAAQ;EACN;CACF;AACF;AAsBO,IAAA,sBAAA,uBAAA,MAAM,oBAAoB;CAOZ;CACqC;CAKzB;CAZ/B;CACA;CACA;CAEA,YACE,UAEA,SACA,QAIA,cACA,aACA;EARiB,KAAA,WAAA;EACqC,KAAA,UAAA;EAKzB,KAAA,eAAA;EAG7B,KAAK,SAAS,UAAU,IAAI,OAAO,cAAc;EACjD,KAAK,WAAW,QAAQ,YAAY,CAAC;EACrC,KAAK,SAAS,eAAe,SAAS,QAAQ,QAAQ,OAAO;CAC/D;;CAGA,IAAI,aAAqB;EACvB,OAAO,KAAK,OAAO;CACrB;;;;;CAMA,IAAI,MAAmC;EACrC,IAAI,SAAS,KAAK,OAAO,MAAM,OAAO;EACtC,OAAO,KAAK,MAAM,EAAE,QAAQ,KAAK,SAAS,IAAI,IAAI,EAAE,CAAC;CACvD;;;;;;CAOA,WAAW,KAA0C;EACnD,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,CAAC;CACpC;;CAKA,MAAM,gBACJ,SACA,cACA,SACkB;EAClB,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,KAAK;EAC3D,OAAO,KAAK,KACV,SACA,YAAY,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,SAC3E,QACR;CACF;;CAGA,MAAM,eACJ,SACA,cACA,SACiB;EACjB,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,EAAE;EACxD,OAAO,KAAK,KACV,SACA,YAAY,KAAK,OAAO,UAAU,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,SAC1E,QACR;CACF;;CAGA,MAAM,eACJ,SACA,cACA,SACiB;EACjB,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,CAAC;EACvD,OAAO,KAAK,KACV,SACA,YAAY,KAAK,OAAO,UAAU,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,SAC1E,QACR;CACF;;CAGA,MAAM,eACJ,SACA,cACA,SACY;EACZ,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,CAAC,CAAM;EAC7D,OAAO,KAAK,KACV,SACA,YAAY,KAAK,OAAO,UAAa,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,SAC7E,QACR;CACF;;CAGA,MAAM,kBACJ,SACA,cACA,SACyC;EACzC,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,KAAK;EAC3D,OAAO,KAAK,KACV,SACA,YACE,KAAK,QACH,SACA,MAAM,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,CAC7E,IACD,UAAU,KAAK,aAAa,SAAS,UAAU,KAAK,CACvD;CACF;;CAGA,MAAM,iBACJ,SACA,cACA,SACwC;EACxC,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,EAAE;EACxD,OAAO,KAAK,KACV,SACA,YACE,KAAK,QACH,SACA,MAAM,KAAK,OAAO,UAAU,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,CAC5E,IACD,UAAU,KAAK,aAAa,SAAS,UAAU,KAAK,CACvD;CACF;;CAGA,MAAM,iBACJ,SACA,cACA,SACwC;EACxC,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,CAAC;EACvD,OAAO,KAAK,KACV,SACA,YACE,KAAK,QACH,SACA,MAAM,KAAK,OAAO,UAAU,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,CAC5E,IACD,UAAU,KAAK,aAAa,SAAS,UAAU,KAAK,CACvD;CACF;;CAGA,MAAM,iBACJ,SACA,cACA,SACmC;EACnC,MAAM,WAAW,KAAK,SAAS,SAAS,cAAc,CAAC,CAAM;EAC7D,OAAO,KAAK,KACV,SACA,YACE,KAAK,QACH,SACA,MAAM,KAAK,OAAO,UAAa,SAAS,UAAU,MAAM,KAAK,QAAQ,OAAO,CAAC,CAC/E,IACD,UAAU,KAAK,aAAa,SAAS,UAAU,KAAK,CACvD;CACF;;;;;;;CAQA,MAAM,IAAI,SAA2D;EACnE,MAAM,OAAO,OAAO,KAAK,KAAK,QAAQ;EACtC,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,KAAK,QAAQ,OAAO;EACrC,SAAS,OAAO;GACd,KAAK,OAAO,KACV,qDAAqD,KAAK,OAAO,KAAK,kCACtE,EAAE,OAAO,QAAQ,KAAK,EAAE,CAC1B;GACA,OAAO,EAAE,GAAG,KAAK,SAAS;EAC5B;EACA,MAAM,SAAS,MAAM,QAAQ,IAC3B,KAAK,KAAK,QAAQ,KAAK,SAAS,KAAK,KAAK,SAAS,MAAO,MAAM,CAAC,CACnE;EACA,MAAM,SAAoC,CAAC;EAC3C,KAAK,SAAS,KAAK,MAAM;GACvB,OAAO,OAAO,OAAO;EACvB,CAAC;EACD,OAAO;CACT;;CAKA,MAAc,WAGU;EACtB,OAAO,IAAA,qBACL,KAAK,UACL,KAAK,SACL,KAAK,QACL,UAAU,WAAW,KAAK,cAC1B,UAAU,UAAU,KAAK,MAC3B;CACF;;CAGA,MAAc,QAAQ,aAA6D;EACjF,MAAM,OAAO,KAAK,gBAAgB,sBAAsB;EACxD,IAAI,CAAC,KAAK,QAAQ,WAAW,CAAC,MAAM,OAAO;EAC3C,MAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,IAAI;EAChD,OAAO,cAAc;GAAE,GAAG;GAAU,GAAG;EAAY,IAAI;CACzD;;CAGA,SAAsC,SAAiB,UAAyB,MAAY;EAC1F,IAAI,aAAa,KAAA,GAAW,OAAO;EACnC,IAAI,WAAW,KAAK,UAAU,OAAO,KAAK,SAAS;EACnD,OAAO;CACT;;CAGA,SACE,SACA,UACA,SACoB;EACpB,QAAQ,OAAO,UAAf;GACE,KAAK,WACH,OAAO,KAAK,KACV,eACM,KAAK,OAAO,WAAW,SAAS,UAAU,OAAO,SACjD,QACR;GACF,KAAK,UACH,OAAO,KAAK,KACV,eACM,KAAK,OAAO,UAAU,SAAS,UAAU,OAAO,SAChD,QACR;GACF,KAAK,UACH,OAAO,KAAK,KACV,eACM,KAAK,OAAO,UAAU,SAAS,UAAU,OAAO,SAChD,QACR;GACF,SACE,OAAO,KAAK,KACV,eACM,KAAK,OAAO,UAAU,SAAS,UAAoB,OAAO,SAC1D,QACR;EACJ;CACF;;;;;CAMA,MAAc,KACZ,SACA,UACA,SACY;EACZ,IAAI;GACF,OAAO,MAAM,SAAS;EACxB,SAAS,OAAO;GACd,KAAK,OAAO,KACV,uCAAuC,QAAQ,eAAe,KAAK,OAAO,KAAK,mCAC/E,EAAE,OAAO,QAAQ,KAAK,EAAE,CAC1B;GACA,OAAO,QAAQ,KAAK;EACtB;CACF;CAEA,QAAqC,SAAiB,OAAoC;EACxF,OAAO;GAAE;GAAS;GAAO,QAAQ;EAAS;CAC5C;CAEA,aACE,SACA,OACA,OAC0B;EAC1B,OAAO;GAAE;GAAS;GAAO,QAAQ;GAAS,cAAc,QAAQ,KAAK;EAAE;CACzE;AACF;;CA3SC,WAAW,EAAE,OAAO,MAAM,UAAU,CAAC;oBAOjC,OAAO,oBAAoB,cAAc,CAAA;oBAEzC,OAAO,oBAAoB,OAAO,CAAA;oBAClC,SAAS,CAAA;oBAAG,OAAO,MAAM,CAAA;oBAIzB,SAAS,CAAA;oBACT,SAAS,CAAA;;;;;;;;;;AA+Rd,SAAS,QAAQ,OAAwB;CACvC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,115 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { a as FeatureFlagDriverRegistry, c as FeatureFlagsOptions, d as FlagEvaluationReason, f as FlagKey, h as FeatureFlagDriver, i as FeatureFlagsService, l as FlagContext, m as FlagValue, n as MemoryFlagDriverOptions, o as buildDriverRegistry, p as FlagManifest, r as memoryFlagDriver, s as FeatureFlagRegistry, t as MemoryFlagDriver, u as FlagEvaluationDetails } from "./memory.driver-C-5Y2b7_.js";
|
|
2
|
+
import { CanActivate, ExecutionContext } from "@velajs/vela";
|
|
3
|
+
//#region src/feature-flags.module.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The feature-flags module. Authored on vela's public `defineModule`, so
|
|
6
|
+
* `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
|
|
7
|
+
* matching `forRootAsync({ inject, useFactory, ... })` come for free.
|
|
8
|
+
*
|
|
9
|
+
* `lazy: true` is valid here: every provider is sync-constructible (a factory
|
|
10
|
+
* for the driver registry, a sync-constructor service, a guard) and there are
|
|
11
|
+
* no async lifecycle hooks — so the module defers to first use without
|
|
12
|
+
* violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
|
|
13
|
+
*
|
|
14
|
+
* `isGlobal: true` makes the module globally visible AND registers
|
|
15
|
+
* {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
|
|
16
|
+
* route is gated without a per-controller `@UseGuards`.
|
|
17
|
+
*/
|
|
18
|
+
declare const ConfigurableModuleClass: import("@velajs/vela").ConfigurableModuleClassType<FeatureFlagsOptions, "forRoot", "create", {
|
|
19
|
+
isGlobal?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
declare class FeatureFlagsModule extends ConfigurableModuleClass {}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/feature-flags.tokens.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* Injection tokens for the feature-flags module. Named on the
|
|
26
|
+
* `vela:feature-flags:<thing>` convention so identity is stable across
|
|
27
|
+
* refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.
|
|
28
|
+
*/
|
|
29
|
+
declare const FEATURE_FLAG_TOKENS: {
|
|
30
|
+
/** The resolved {@link FeatureFlagsOptions} (module options token). */
|
|
31
|
+
readonly Options: import("@velajs/vela").InjectionToken<FeatureFlagsOptions>;
|
|
32
|
+
/** The injectable {@link FeatureFlagsService}. */
|
|
33
|
+
readonly Service: import("@velajs/vela").InjectionToken<FeatureFlagsService>;
|
|
34
|
+
/** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */
|
|
35
|
+
readonly DriverRegistry: import("@velajs/vela").InjectionToken<FeatureFlagDriverRegistry>;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/guards/feature-flag.guard.d.ts
|
|
39
|
+
/**
|
|
40
|
+
* Route gate for `@FeatureFlag()`. Reads the handler/controller metadata, then
|
|
41
|
+
* `getBooleanValue(key)`; when the flag is off it throws `NotFoundException`
|
|
42
|
+
* (the route appears hidden) or `ForbiddenException` per the decorator option.
|
|
43
|
+
* Handlers with no `@FeatureFlag()` metadata pass through untouched, so the
|
|
44
|
+
* guard is safe to register app-wide (`FeatureFlagsModule.forRoot({ isGlobal:
|
|
45
|
+
* true })`) or per-route via `@UseGuards(FeatureFlagGuard)`.
|
|
46
|
+
*
|
|
47
|
+
* It does NOT inject `REQUEST_CONTEXT` — that would make the guard request-
|
|
48
|
+
* scoped and break lazy-module materialization (which constructs every provider
|
|
49
|
+
* once, at first use, possibly outside a request). Instead it reads the request
|
|
50
|
+
* context off the per-request child container carried on the Hono context, so
|
|
51
|
+
* the module's `context` resolver still runs for the gate decision.
|
|
52
|
+
*/
|
|
53
|
+
declare class FeatureFlagGuard implements CanActivate {
|
|
54
|
+
private readonly flags;
|
|
55
|
+
private readonly reflector;
|
|
56
|
+
constructor(flags: FeatureFlagsService);
|
|
57
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
58
|
+
/** The current request context, via the child container on the Hono context. */
|
|
59
|
+
private requestContext;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/decorators/feature-flag.decorator.d.ts
|
|
63
|
+
/** What the {@link FeatureFlagGuard} does when a gated flag is off. */
|
|
64
|
+
type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';
|
|
65
|
+
interface FeatureFlagOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Response when the flag is off. `'notFound'` (default) hides the route
|
|
68
|
+
* entirely (404); `'forbidden'` reveals it exists but denies access (403).
|
|
69
|
+
*/
|
|
70
|
+
onDisabled?: FeatureFlagDisabledBehavior;
|
|
71
|
+
}
|
|
72
|
+
/** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */
|
|
73
|
+
interface FeatureFlagMetadata {
|
|
74
|
+
key: string;
|
|
75
|
+
onDisabled: FeatureFlagDisabledBehavior;
|
|
76
|
+
}
|
|
77
|
+
declare const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
|
|
78
|
+
/**
|
|
79
|
+
* Gate a route (handler) or controller behind a boolean feature flag. Pair
|
|
80
|
+
* with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
|
|
81
|
+
* app-wide registration).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* @UseGuards(FeatureFlagGuard)
|
|
86
|
+
* @Controller('/checkout')
|
|
87
|
+
* class CheckoutController {
|
|
88
|
+
* @FeatureFlag('new-checkout') // 404 when off
|
|
89
|
+
* @Get('/v2') v2() { ... }
|
|
90
|
+
*
|
|
91
|
+
* @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
92
|
+
* @Get('/beta') beta() { ... }
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function FeatureFlag(key: FlagKey, options?: FeatureFlagOptions): (target: object, propertyKey?: string | symbol) => void;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/feature-flags.error.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* The single error type for `@velajs/feature-flags`.
|
|
101
|
+
*
|
|
102
|
+
* Note: flag *evaluation* never throws — the service absorbs driver failures
|
|
103
|
+
* into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
|
|
104
|
+
* is reserved for *configuration* faults surfaced at wiring time: an unknown
|
|
105
|
+
* driver name, a duplicate driver, or an empty driver set.
|
|
106
|
+
*/
|
|
107
|
+
declare class FeatureFlagError extends Error {
|
|
108
|
+
name: string;
|
|
109
|
+
constructor(message: string, options?: {
|
|
110
|
+
cause?: unknown;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { FEATURE_FLAG_METADATA, FEATURE_FLAG_TOKENS, FeatureFlag, type FeatureFlagDisabledBehavior, type FeatureFlagDriver, FeatureFlagDriverRegistry, FeatureFlagError, FeatureFlagGuard, type FeatureFlagMetadata, type FeatureFlagOptions, type FeatureFlagRegistry, FeatureFlagsModule, type FeatureFlagsOptions, FeatureFlagsService, type FlagContext, type FlagEvaluationDetails, type FlagEvaluationReason, type FlagKey, type FlagManifest, type FlagValue, MemoryFlagDriver, type MemoryFlagDriverOptions, buildDriverRegistry, memoryFlagDriver };
|
|
115
|
+
//# sourceMappingURL=index.d.ts.map
|