@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.
@@ -1,26 +0,0 @@
1
- import { SetMetadata } from "@velajs/vela";
2
- export const FEATURE_FLAG_METADATA = 'vela:feature-flags:flag';
3
- /**
4
- * Gate a route (handler) or controller behind a boolean feature flag. Pair
5
- * with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
6
- * app-wide registration).
7
- *
8
- * @example
9
- * ```ts
10
- * @UseGuards(FeatureFlagGuard)
11
- * @Controller('/checkout')
12
- * class CheckoutController {
13
- * @FeatureFlag('new-checkout') // 404 when off
14
- * @Get('/v2') v2() { ... }
15
- *
16
- * @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
17
- * @Get('/beta') beta() { ... }
18
- * }
19
- * ```
20
- */ export function FeatureFlag(key, options = {}) {
21
- const meta = {
22
- key,
23
- onDisabled: options.onDisabled ?? 'notFound'
24
- };
25
- return SetMetadata(FEATURE_FLAG_METADATA, meta);
26
- }
@@ -1,21 +0,0 @@
1
- import type { FlagContext } from '../feature-flags.types';
2
- /**
3
- * The cross-package contract every feature-flag backend implements.
4
- *
5
- * This is the seam that platform packages plug into: `@velajs/cloudflare`
6
- * ships a Flagship-binding driver and a KV-backed driver against this exact
7
- * interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
8
- * maps a key + fallback (+ optional targeting context) onto a typed value and
9
- * MUST return the `fallback` — never throw — when it cannot resolve the key.
10
- *
11
- * Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
12
- * `FeatureFlagsService` around these four value methods; drivers may
13
- * optionally override that synthesis, but the value methods are the contract.
14
- */
15
- export interface FeatureFlagDriver {
16
- readonly name: string;
17
- getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
18
- getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
19
- getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
20
- getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
21
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * The cross-package contract every feature-flag backend implements.
3
- *
4
- * This is the seam that platform packages plug into: `@velajs/cloudflare`
5
- * ships a Flagship-binding driver and a KV-backed driver against this exact
6
- * interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
7
- * maps a key + fallback (+ optional targeting context) onto a typed value and
8
- * MUST return the `fallback` — never throw — when it cannot resolve the key.
9
- *
10
- * Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
11
- * `FeatureFlagsService` around these four value methods; drivers may
12
- * optionally override that synthesis, but the value methods are the contract.
13
- */ export { };
@@ -1,35 +0,0 @@
1
- import type { FeatureFlagDriver } from './driver';
2
- import type { FlagContext, FlagManifest, FlagValue } from '../feature-flags.types';
3
- export interface MemoryFlagDriverOptions {
4
- /** Driver name used for `use(name)` / the default-driver selection. Default `"memory"`. */
5
- name?: string;
6
- /** Initial flag values. */
7
- values?: FlagManifest;
8
- }
9
- /**
10
- * In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
11
- * and the test fake in one. Reads from an internal `Map` seeded from options
12
- * or mutated with {@link set}/{@link reset} — the memory driver ignores the
13
- * evaluation context (it does no targeting). An unknown key returns the
14
- * caller's `fallback`, exactly like a remote driver that can't resolve it.
15
- */
16
- export declare class MemoryFlagDriver implements FeatureFlagDriver {
17
- readonly name: string;
18
- private readonly store;
19
- constructor(options?: MemoryFlagDriverOptions);
20
- /** Set a flag value. Chainable. */
21
- set(key: string, value: FlagValue): this;
22
- /** Remove a flag (subsequent reads return the caller's fallback). Chainable. */
23
- delete(key: string): this;
24
- /** True when `key` has a stored value. */
25
- has(key: string): boolean;
26
- /** Clear all flags, then optionally seed a fresh set. Chainable. */
27
- reset(values?: FlagManifest): this;
28
- getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
29
- getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
30
- getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
31
- getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
32
- private read;
33
- }
34
- /** Convenience factory for {@link MemoryFlagDriver}. */
35
- export declare function memoryFlagDriver(options?: MemoryFlagDriverOptions): MemoryFlagDriver;
@@ -1,50 +0,0 @@
1
- /**
2
- * In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
3
- * and the test fake in one. Reads from an internal `Map` seeded from options
4
- * or mutated with {@link set}/{@link reset} — the memory driver ignores the
5
- * evaluation context (it does no targeting). An unknown key returns the
6
- * caller's `fallback`, exactly like a remote driver that can't resolve it.
7
- */ export class MemoryFlagDriver {
8
- name;
9
- store;
10
- constructor(options = {}){
11
- this.name = options.name ?? 'memory';
12
- this.store = new Map(Object.entries(options.values ?? {}));
13
- }
14
- /** Set a flag value. Chainable. */ set(key, value) {
15
- this.store.set(key, value);
16
- return this;
17
- }
18
- /** Remove a flag (subsequent reads return the caller's fallback). Chainable. */ delete(key) {
19
- this.store.delete(key);
20
- return this;
21
- }
22
- /** True when `key` has a stored value. */ has(key) {
23
- return this.store.has(key);
24
- }
25
- /** Clear all flags, then optionally seed a fresh set. Chainable. */ reset(values) {
26
- this.store.clear();
27
- if (values) {
28
- for (const [key, value] of Object.entries(values))this.store.set(key, value);
29
- }
30
- return this;
31
- }
32
- getBoolean(key, fallback, _ctx) {
33
- return Promise.resolve(this.read(key, fallback));
34
- }
35
- getString(key, fallback, _ctx) {
36
- return Promise.resolve(this.read(key, fallback));
37
- }
38
- getNumber(key, fallback, _ctx) {
39
- return Promise.resolve(this.read(key, fallback));
40
- }
41
- getObject(key, fallback, _ctx) {
42
- return Promise.resolve(this.read(key, fallback));
43
- }
44
- read(key, fallback) {
45
- return this.store.has(key) ? this.store.get(key) : fallback;
46
- }
47
- }
48
- /** Convenience factory for {@link MemoryFlagDriver}. */ export function memoryFlagDriver(options) {
49
- return new MemoryFlagDriver(options);
50
- }
@@ -1,24 +0,0 @@
1
- import type { FeatureFlagsOptions } from '../feature-flags.types';
2
- import type { FeatureFlagDriver } from './driver';
3
- /**
4
- * The set of registered {@link FeatureFlagDriver}s plus the default selection.
5
- * Built once per module instance ({@link buildDriverRegistry}) and injected
6
- * into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
7
- */
8
- export declare class FeatureFlagDriverRegistry {
9
- private readonly drivers;
10
- readonly defaultName: string;
11
- constructor(drivers: readonly FeatureFlagDriver[], defaultName?: string);
12
- /** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */
13
- get(name: string): FeatureFlagDriver;
14
- /** Resolve a named driver, or the default when `name` is omitted. */
15
- resolve(name?: string): FeatureFlagDriver;
16
- /** All registered driver names. */
17
- names(): string[];
18
- }
19
- /**
20
- * Build the driver registry from module options. Falls back to a single
21
- * in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
22
- * yields a working (all-defaults) flags service for local development.
23
- */
24
- export declare function buildDriverRegistry(options: FeatureFlagsOptions): FeatureFlagDriverRegistry;
@@ -1,51 +0,0 @@
1
- import { FeatureFlagError } from "../feature-flags.error.js";
2
- import { MemoryFlagDriver } from "./memory.driver.js";
3
- /**
4
- * The set of registered {@link FeatureFlagDriver}s plus the default selection.
5
- * Built once per module instance ({@link buildDriverRegistry}) and injected
6
- * into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
7
- */ export class FeatureFlagDriverRegistry {
8
- drivers = new Map();
9
- defaultName;
10
- constructor(drivers, defaultName){
11
- if (drivers.length === 0) {
12
- throw new FeatureFlagError('No feature flag drivers registered. Provide at least one driver to FeatureFlagsModule.forRoot({ drivers: [...] }).');
13
- }
14
- for (const driver of drivers){
15
- if (this.drivers.has(driver.name)) {
16
- throw new FeatureFlagError(`Duplicate feature flag driver "${driver.name}".`);
17
- }
18
- this.drivers.set(driver.name, driver);
19
- }
20
- const requested = defaultName ?? drivers[0].name;
21
- if (!this.drivers.has(requested)) {
22
- throw new FeatureFlagError(`Default feature flag driver "${requested}" is not registered.`);
23
- }
24
- this.defaultName = requested;
25
- }
26
- /** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */ get(name) {
27
- const driver = this.drivers.get(name);
28
- if (!driver) {
29
- throw new FeatureFlagError(`Feature flag driver "${name}" is not registered.`);
30
- }
31
- return driver;
32
- }
33
- /** Resolve a named driver, or the default when `name` is omitted. */ resolve(name) {
34
- return this.get(name ?? this.defaultName);
35
- }
36
- /** All registered driver names. */ names() {
37
- return [
38
- ...this.drivers.keys()
39
- ];
40
- }
41
- }
42
- /**
43
- * Build the driver registry from module options. Falls back to a single
44
- * in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
45
- * yields a working (all-defaults) flags service for local development.
46
- */ export function buildDriverRegistry(options) {
47
- const drivers = options.drivers && options.drivers.length > 0 ? options.drivers : [
48
- new MemoryFlagDriver()
49
- ];
50
- return new FeatureFlagDriverRegistry(drivers, options.default);
51
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * The single error type for `@velajs/feature-flags`.
3
- *
4
- * Note: flag *evaluation* never throws — the service absorbs driver failures
5
- * into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
6
- * is reserved for *configuration* faults surfaced at wiring time: an unknown
7
- * driver name, a duplicate driver, or an empty driver set.
8
- */
9
- export declare class FeatureFlagError extends Error {
10
- name: string;
11
- constructor(message: string, options?: {
12
- cause?: unknown;
13
- });
14
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * The single error type for `@velajs/feature-flags`.
3
- *
4
- * Note: flag *evaluation* never throws — the service absorbs driver failures
5
- * into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
6
- * is reserved for *configuration* faults surfaced at wiring time: an unknown
7
- * driver name, a duplicate driver, or an empty driver set.
8
- */ export class FeatureFlagError extends Error {
9
- name = 'FeatureFlagError';
10
- constructor(message, options){
11
- super(message, options?.cause !== undefined ? {
12
- cause: options.cause
13
- } : undefined);
14
- }
15
- }
@@ -1,21 +0,0 @@
1
- import type { FeatureFlagsOptions } from './feature-flags.types';
2
- /**
3
- * The feature-flags module. Authored on vela's public `defineModule`, so
4
- * `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
5
- * matching `forRootAsync({ inject, useFactory, ... })` come for free.
6
- *
7
- * `lazy: true` is valid here: every provider is sync-constructible (a factory
8
- * for the driver registry, a sync-constructor service, a guard) and there are
9
- * no async lifecycle hooks — so the module defers to first use without
10
- * violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
11
- *
12
- * `isGlobal: true` makes the module globally visible AND registers
13
- * {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
14
- * route is gated without a per-controller `@UseGuards`.
15
- */
16
- declare const ConfigurableModuleClass: import("@velajs/vela").ConfigurableModuleClassType<FeatureFlagsOptions, "forRoot", "create", {
17
- isGlobal?: boolean;
18
- }>;
19
- export declare class FeatureFlagsModule extends ConfigurableModuleClass {
20
- }
21
- export {};
@@ -1,63 +0,0 @@
1
- import { APP_GUARD, Scope, defineModule } from "@velajs/vela";
2
- import { buildDriverRegistry } from "./drivers/registry.js";
3
- import { FeatureFlagGuard } from "./guards/feature-flag.guard.js";
4
- import { FeatureFlagsService } from "./feature-flags.service.js";
5
- import { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
6
- /**
7
- * The feature-flags module. Authored on vela's public `defineModule`, so
8
- * `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
9
- * matching `forRootAsync({ inject, useFactory, ... })` come for free.
10
- *
11
- * `lazy: true` is valid here: every provider is sync-constructible (a factory
12
- * for the driver registry, a sync-constructor service, a guard) and there are
13
- * no async lifecycle hooks — so the module defers to first use without
14
- * violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
15
- *
16
- * `isGlobal: true` makes the module globally visible AND registers
17
- * {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
18
- * route is gated without a per-controller `@UseGuards`.
19
- */ const { ConfigurableModuleClass } = defineModule({
20
- name: 'FeatureFlags',
21
- optionsToken: FEATURE_FLAG_TOKENS.Options,
22
- lazy: true,
23
- transform: (definition, extras)=>extras.isGlobal ? {
24
- ...definition,
25
- global: true,
26
- providers: [
27
- ...definition.providers ?? [],
28
- {
29
- provide: APP_GUARD,
30
- useExisting: FeatureFlagGuard
31
- }
32
- ]
33
- } : definition,
34
- setup: ({ OPTIONS })=>({
35
- providers: [
36
- {
37
- provide: FEATURE_FLAG_TOKENS.DriverRegistry,
38
- useFactory: (options)=>buildDriverRegistry(options),
39
- inject: [
40
- OPTIONS
41
- ]
42
- },
43
- // Transient: a fresh service per injection, so `use()`/`forRequest()`
44
- // clones and per-request context stay isolated. The service never injects
45
- // REQUEST_CONTEXT, so it does not bubble to request scope and remains
46
- // resolvable in queue / scheduled / global scope.
47
- {
48
- provide: FEATURE_FLAG_TOKENS.Service,
49
- useClass: FeatureFlagsService,
50
- scope: Scope.TRANSIENT
51
- },
52
- FeatureFlagGuard
53
- ],
54
- exports: [
55
- FEATURE_FLAG_TOKENS.Service,
56
- FEATURE_FLAG_TOKENS.DriverRegistry,
57
- FeatureFlagGuard,
58
- OPTIONS
59
- ]
60
- })
61
- });
62
- export class FeatureFlagsModule extends ConfigurableModuleClass {
63
- }
@@ -1,83 +0,0 @@
1
- import { type LoggerService, type RequestContext } from '@velajs/vela';
2
- import type { FeatureFlagDriver } from './drivers/driver';
3
- import type { FeatureFlagDriverRegistry } from './drivers/registry';
4
- import type { FeatureFlagsOptions, FlagContext, FlagEvaluationDetails, FlagKey, FlagValue } from './feature-flags.types';
5
- /**
6
- * The injectable consumers reach for. A thin, type-safe, never-throw wrapper
7
- * over a {@link FeatureFlagDriver}, with two ergonomic additions:
8
- *
9
- * - **Manifest defaults** — omit a default and the value declared in the app's
10
- * `manifest` is used (an explicit argument always wins).
11
- * - **Default context** — the module's `context` resolver is merged into every
12
- * evaluation (per-call context overrides it), resolved from the current
13
- * request and skipped automatically outside request scope.
14
- *
15
- * Switch drivers with {@link use}; bind a request with {@link forRequest} (the
16
- * guard does this). Evaluation NEVER throws — the driver returns the fallback
17
- * on evaluation errors, and the service additionally absorbs any thrown error
18
- * into the same fallback with a logged warning.
19
- *
20
- * `@Transient`: constructed synchronously (lazy-module-safe), a fresh instance
21
- * per injection, and resolvable in and out of request scope — it never
22
- * DI-injects `REQUEST_CONTEXT`, so it does not bubble to request scope.
23
- */
24
- export declare class FeatureFlagsService {
25
- private readonly registry;
26
- private readonly options;
27
- private readonly boundContext?;
28
- private readonly logger;
29
- private readonly manifest;
30
- private readonly driver;
31
- constructor(registry: FeatureFlagDriverRegistry, options: FeatureFlagsOptions, logger?: LoggerService, boundContext?: RequestContext | undefined, boundDriver?: FeatureFlagDriver);
32
- /** The name of the driver this instance targets. */
33
- get driverName(): string;
34
- /**
35
- * Switch to a different registered driver. Returns a NEW immutable instance
36
- * bound to `name`; the original is unchanged. Throws if `name` is unknown.
37
- */
38
- use(name: string): FeatureFlagsService;
39
- /**
40
- * Bind a request context. Returns a NEW immutable instance whose evaluations
41
- * merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
42
- * resolved in request scope can also call it explicitly.
43
- */
44
- forRequest(ctx: RequestContext): FeatureFlagsService;
45
- /** Evaluate a flag as a `boolean`. */
46
- getBooleanValue(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<boolean>;
47
- /** Evaluate a flag as a `string`. */
48
- getStringValue(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<string>;
49
- /** Evaluate a flag as a `number`. */
50
- getNumberValue(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<number>;
51
- /** Evaluate a flag as a typed object. */
52
- getObjectValue<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<T>;
53
- /** Evaluate a `boolean` flag with synthesized evaluation metadata. */
54
- getBooleanDetails(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<FlagEvaluationDetails<boolean>>;
55
- /** Evaluate a `string` flag with synthesized evaluation metadata. */
56
- getStringDetails(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<FlagEvaluationDetails<string>>;
57
- /** Evaluate a `number` flag with synthesized evaluation metadata. */
58
- getNumberDetails(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<FlagEvaluationDetails<number>>;
59
- /** Evaluate a typed object flag with synthesized evaluation metadata. */
60
- getObjectDetails<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<FlagEvaluationDetails<T>>;
61
- /**
62
- * Evaluate every flag declared in the manifest and return a `{ key: value }`
63
- * map. The evaluation method is chosen from each declared default's type.
64
- * A throwing context resolver falls back to the manifest defaults rather than
65
- * taking the batch down.
66
- */
67
- all(context?: FlagContext): Promise<Record<string, FlagValue>>;
68
- /** Immutable clone with a different driver and/or bound context. */
69
- private clone;
70
- /** Resolve the merged evaluation context (default context + per-call override). */
71
- private context;
72
- /** Pick the default: explicit arg, then manifest, then the type's zero value. */
73
- private fallback;
74
- /** Evaluate a single flag, choosing the method from the declared default's type. */
75
- private evaluate;
76
- /**
77
- * Run an evaluation and absorb any failure into the fallback. A flag lookup
78
- * must never take the caller down with it.
79
- */
80
- private safe;
81
- private details;
82
- private errorDetails;
83
- }
@@ -1,208 +0,0 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- 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;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- }
7
- function _ts_metadata(k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- }
10
- function _ts_param(paramIndex, decorator) {
11
- return function(target, key) {
12
- decorator(target, key, paramIndex);
13
- };
14
- }
15
- // Never-throw + manifest-default ergonomics adapted from
16
- // @stratal/feature-flags (MIT, © Temitayo Fadojutimi), reshaped from a single
17
- // Cloudflare-binding service into this driver-based, edge-pure form.
18
- import { Inject, Injectable, Logger, Optional, Scope, getCurrentRequestContext } from "@velajs/vela";
19
- import { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
20
- /**
21
- * Safely read the current request context. Returns `undefined` outside a
22
- * request or when ambient access isn't enabled — never throws — so the service
23
- * resolves and evaluates in queue / scheduled / global scope too.
24
- */ function ambientRequestContext() {
25
- try {
26
- return getCurrentRequestContext();
27
- } catch {
28
- return undefined;
29
- }
30
- }
31
- export class FeatureFlagsService {
32
- registry;
33
- options;
34
- boundContext;
35
- logger;
36
- manifest;
37
- driver;
38
- constructor(registry, options, logger, // The next two are never provided by DI (they carry `@Optional()` so the
39
- // container leaves them `undefined`). `forRequest()` and `use()` set them
40
- // when cloning; tests may pass them directly.
41
- boundContext, boundDriver){
42
- this.registry = registry;
43
- this.options = options;
44
- this.boundContext = boundContext;
45
- this.logger = logger ?? new Logger('FeatureFlags');
46
- this.manifest = options.manifest ?? {};
47
- this.driver = boundDriver ?? registry.resolve(options.default);
48
- }
49
- /** The name of the driver this instance targets. */ get driverName() {
50
- return this.driver.name;
51
- }
52
- /**
53
- * Switch to a different registered driver. Returns a NEW immutable instance
54
- * bound to `name`; the original is unchanged. Throws if `name` is unknown.
55
- */ use(name) {
56
- if (name === this.driver.name) return this;
57
- return this.clone({
58
- driver: this.registry.get(name)
59
- });
60
- }
61
- /**
62
- * Bind a request context. Returns a NEW immutable instance whose evaluations
63
- * merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
64
- * resolved in request scope can also call it explicitly.
65
- */ forRequest(ctx) {
66
- return this.clone({
67
- context: ctx
68
- });
69
- }
70
- // ==================== EVALUATION ====================
71
- /** Evaluate a flag as a `boolean`. */ async getBooleanValue(flagKey, defaultValue, context) {
72
- const fallback = this.fallback(flagKey, defaultValue, false);
73
- return this.safe(flagKey, async ()=>this.driver.getBoolean(flagKey, fallback, await this.context(context)), ()=>fallback);
74
- }
75
- /** Evaluate a flag as a `string`. */ async getStringValue(flagKey, defaultValue, context) {
76
- const fallback = this.fallback(flagKey, defaultValue, '');
77
- return this.safe(flagKey, async ()=>this.driver.getString(flagKey, fallback, await this.context(context)), ()=>fallback);
78
- }
79
- /** Evaluate a flag as a `number`. */ async getNumberValue(flagKey, defaultValue, context) {
80
- const fallback = this.fallback(flagKey, defaultValue, 0);
81
- return this.safe(flagKey, async ()=>this.driver.getNumber(flagKey, fallback, await this.context(context)), ()=>fallback);
82
- }
83
- /** Evaluate a flag as a typed object. */ async getObjectValue(flagKey, defaultValue, context) {
84
- const fallback = this.fallback(flagKey, defaultValue, {});
85
- return this.safe(flagKey, async ()=>this.driver.getObject(flagKey, fallback, await this.context(context)), ()=>fallback);
86
- }
87
- /** Evaluate a `boolean` flag with synthesized evaluation metadata. */ async getBooleanDetails(flagKey, defaultValue, context) {
88
- const fallback = this.fallback(flagKey, defaultValue, false);
89
- return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getBoolean(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
90
- }
91
- /** Evaluate a `string` flag with synthesized evaluation metadata. */ async getStringDetails(flagKey, defaultValue, context) {
92
- const fallback = this.fallback(flagKey, defaultValue, '');
93
- return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getString(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
94
- }
95
- /** Evaluate a `number` flag with synthesized evaluation metadata. */ async getNumberDetails(flagKey, defaultValue, context) {
96
- const fallback = this.fallback(flagKey, defaultValue, 0);
97
- return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getNumber(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
98
- }
99
- /** Evaluate a typed object flag with synthesized evaluation metadata. */ async getObjectDetails(flagKey, defaultValue, context) {
100
- const fallback = this.fallback(flagKey, defaultValue, {});
101
- return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getObject(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
102
- }
103
- /**
104
- * Evaluate every flag declared in the manifest and return a `{ key: value }`
105
- * map. The evaluation method is chosen from each declared default's type.
106
- * A throwing context resolver falls back to the manifest defaults rather than
107
- * taking the batch down.
108
- */ async all(context) {
109
- const keys = Object.keys(this.manifest);
110
- let merged;
111
- try {
112
- merged = await this.context(context);
113
- } catch (error) {
114
- this.logger.warn(`Feature flag context resolution failed on driver "${this.driver.name}"; returning manifest defaults.`, {
115
- error: message(error)
116
- });
117
- return {
118
- ...this.manifest
119
- };
120
- }
121
- const values = await Promise.all(keys.map((key)=>this.evaluate(key, this.manifest[key], merged)));
122
- const result = {};
123
- keys.forEach((key, i)=>{
124
- result[key] = values[i];
125
- });
126
- return result;
127
- }
128
- // ==================== INTERNAL ====================
129
- /** Immutable clone with a different driver and/or bound context. */ clone(overrides) {
130
- return new FeatureFlagsService(this.registry, this.options, this.logger, overrides.context ?? this.boundContext, overrides.driver ?? this.driver);
131
- }
132
- /** Resolve the merged evaluation context (default context + per-call override). */ async context(callContext) {
133
- const base = this.boundContext ?? ambientRequestContext();
134
- if (!this.options.context || !base) return callContext;
135
- const resolved = await this.options.context(base);
136
- return callContext ? {
137
- ...resolved,
138
- ...callContext
139
- } : resolved;
140
- }
141
- /** Pick the default: explicit arg, then manifest, then the type's zero value. */ fallback(flagKey, provided, zero) {
142
- if (provided !== undefined) return provided;
143
- if (flagKey in this.manifest) return this.manifest[flagKey];
144
- return zero;
145
- }
146
- /** Evaluate a single flag, choosing the method from the declared default's type. */ evaluate(flagKey, declared, context) {
147
- switch(typeof declared){
148
- case 'boolean':
149
- return this.safe(flagKey, ()=>this.driver.getBoolean(flagKey, declared, context), ()=>declared);
150
- case 'number':
151
- return this.safe(flagKey, ()=>this.driver.getNumber(flagKey, declared, context), ()=>declared);
152
- case 'string':
153
- return this.safe(flagKey, ()=>this.driver.getString(flagKey, declared, context), ()=>declared);
154
- default:
155
- return this.safe(flagKey, ()=>this.driver.getObject(flagKey, declared, context), ()=>declared);
156
- }
157
- }
158
- /**
159
- * Run an evaluation and absorb any failure into the fallback. A flag lookup
160
- * must never take the caller down with it.
161
- */ async safe(flagKey, evaluate, onError) {
162
- try {
163
- return await evaluate();
164
- } catch (error) {
165
- this.logger.warn(`Feature flag evaluation failed for "${flagKey}" on driver "${this.driver.name}"; returning the fallback value.`, {
166
- error: message(error)
167
- });
168
- return onError(error);
169
- }
170
- }
171
- details(flagKey, value) {
172
- return {
173
- flagKey,
174
- value,
175
- reason: 'STATIC'
176
- };
177
- }
178
- errorDetails(flagKey, value, error) {
179
- return {
180
- flagKey,
181
- value,
182
- reason: 'ERROR',
183
- errorMessage: message(error)
184
- };
185
- }
186
- }
187
- FeatureFlagsService = _ts_decorate([
188
- Injectable({
189
- scope: Scope.TRANSIENT
190
- }),
191
- _ts_param(0, Inject(FEATURE_FLAG_TOKENS.DriverRegistry)),
192
- _ts_param(1, Inject(FEATURE_FLAG_TOKENS.Options)),
193
- _ts_param(2, Optional()),
194
- _ts_param(2, Inject(Logger)),
195
- _ts_param(3, Optional()),
196
- _ts_param(4, Optional()),
197
- _ts_metadata("design:type", Function),
198
- _ts_metadata("design:paramtypes", [
199
- typeof FeatureFlagDriverRegistry === "undefined" ? Object : FeatureFlagDriverRegistry,
200
- typeof FeatureFlagsOptions === "undefined" ? Object : FeatureFlagsOptions,
201
- typeof LoggerService === "undefined" ? Object : LoggerService,
202
- typeof RequestContext === "undefined" ? Object : RequestContext,
203
- typeof FeatureFlagDriver === "undefined" ? Object : FeatureFlagDriver
204
- ])
205
- ], FeatureFlagsService);
206
- /** Extract a human-readable message from an unknown thrown value. */ function message(error) {
207
- return error instanceof Error ? error.message : String(error);
208
- }
@@ -1,16 +0,0 @@
1
- import type { FeatureFlagDriverRegistry } from './drivers/registry';
2
- import type { FeatureFlagsService } from './feature-flags.service';
3
- import type { FeatureFlagsOptions } from './feature-flags.types';
4
- /**
5
- * Injection tokens for the feature-flags module. Named on the
6
- * `vela:feature-flags:<thing>` convention so identity is stable across
7
- * refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.
8
- */
9
- export declare const FEATURE_FLAG_TOKENS: {
10
- /** The resolved {@link FeatureFlagsOptions} (module options token). */
11
- readonly Options: import("@velajs/vela").InjectionToken<FeatureFlagsOptions>;
12
- /** The injectable {@link FeatureFlagsService}. */
13
- readonly Service: import("@velajs/vela").InjectionToken<FeatureFlagsService>;
14
- /** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */
15
- readonly DriverRegistry: import("@velajs/vela").InjectionToken<FeatureFlagDriverRegistry>;
16
- };