@stacksjs/feature-flags 0.70.90
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/LICENSE.md +21 -0
- package/README.md +70 -0
- package/dist/drivers/database.d.ts +12 -0
- package/dist/drivers/index.d.ts +2 -0
- package/dist/drivers/memory.d.ts +14 -0
- package/dist/errors.d.ts +18 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +2 -0
- package/dist/manager.d.ts +42 -0
- package/dist/schema.d.ts +14 -0
- package/dist/scope.d.ts +3 -0
- package/dist/strategies.d.ts +7 -0
- package/dist/types.d.ts +50 -0
- package/dist/value.d.ts +3 -0
- package/package.json +61 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @stacksjs/feature-flags
|
|
2
|
+
|
|
3
|
+
Typed, scoped feature flags for Stacks applications. The API follows the useful parts of Laravel Pennant while keeping storage and scope handling explicit for TypeScript.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Feature, percentage, variants } from '@stacksjs/feature-flags'
|
|
7
|
+
|
|
8
|
+
Feature.define('new-checkout', percentage(10))
|
|
9
|
+
Feature.define('search-layout', variants({ control: 50, compact: 30, visual: 20 }))
|
|
10
|
+
Feature.define('team-reports', team => team.plan === 'pro')
|
|
11
|
+
|
|
12
|
+
if (await Feature.for(user).active('new-checkout')) {
|
|
13
|
+
// Render the new checkout.
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const layout = await Feature.value('search-layout', user)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Scopes are type-aware. Stacks ORM instances use their model definition and ID automatically. Plain objects with an `id` should provide `featureFlagType` when IDs can overlap across domain types:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
await Feature.for({ id: account.id, featureFlagType: 'Account' }).active('team-reports')
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Resolved values are sticky per scope. The first evaluation is stored by the configured driver and reused until it is changed or forgotten.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
await Feature.for(user).activate('new-checkout')
|
|
29
|
+
await Feature.for(user).deactivate('new-checkout')
|
|
30
|
+
await Feature.for(user).forget('new-checkout') // evaluate its definition again
|
|
31
|
+
await Feature.purge('new-checkout') // forget it for every scope
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
`config/feature-flags.ts` selects the global facade's driver:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { defineFeatureFlags } from '@stacksjs/config'
|
|
40
|
+
|
|
41
|
+
export default defineFeatureFlags({
|
|
42
|
+
default: 'database',
|
|
43
|
+
missing: 'false',
|
|
44
|
+
drivers: {
|
|
45
|
+
memory: { cloneValues: true },
|
|
46
|
+
database: { table: 'feature_flags', autoCreate: false },
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The schema belongs to this package. It never writes into an application's `database/migrations` directory implicitly. Applications can explicitly provision it, opt into lazy creation, or publish the generated SQL through their own migration workflow:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { db } from '@stacksjs/database'
|
|
55
|
+
import { ensureFeatureFlagTable, featureFlagMigrationSql } from '@stacksjs/feature-flags'
|
|
56
|
+
|
|
57
|
+
await ensureFeatureFlagTable(db, { dialect: 'sqlite', table: 'feature_flags' })
|
|
58
|
+
const statements = featureFlagMigrationSql({ dialect: 'postgres' })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Set `autoCreate: true` only when runtime DDL is appropriate for the application. Production applications will usually publish `featureFlagMigrationSql()` into their migration workflow and leave automatic creation disabled.
|
|
62
|
+
|
|
63
|
+
Use an isolated manager in tests:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createFeatureFlags } from '@stacksjs/feature-flags'
|
|
67
|
+
|
|
68
|
+
const flags = createFeatureFlags()
|
|
69
|
+
flags.define({ checkout: true, recommendations: false })
|
|
70
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DatabaseFeatureFlagDriverOptions, FeatureDatabaseClient, FeatureDriver, FeatureValue } from '../types';
|
|
2
|
+
export declare function createDatabaseFeatureFlagDriver(database: FeatureDatabaseClient, options?: DatabaseFeatureFlagDriverOptions): DatabaseFeatureFlagDriver;
|
|
3
|
+
export declare class DatabaseFeatureFlagDriver implements FeatureDriver {
|
|
4
|
+
readonly table: string;
|
|
5
|
+
constructor(database: FeatureDatabaseClient, options?: DatabaseFeatureFlagDriverOptions);
|
|
6
|
+
get(name: string, scopeKey: string): Promise<FeatureValue | undefined>;
|
|
7
|
+
set(name: string, scopeKey: string, value: FeatureValue): Promise<void>;
|
|
8
|
+
delete(name: string, scopeKey: string): Promise<void>;
|
|
9
|
+
deleteForAllScopes(names?: readonly string[]): Promise<void>;
|
|
10
|
+
clear(): Promise<void>;
|
|
11
|
+
stored(scopeKey: string): Promise<Record<string, FeatureValue>>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeatureDriver, FeatureValue } from '../types';
|
|
2
|
+
export declare function createMemoryFeatureFlagDriver(options?: MemoryFeatureFlagDriverOptions): MemoryFeatureFlagDriver;
|
|
3
|
+
export declare interface MemoryFeatureFlagDriverOptions {
|
|
4
|
+
cloneValues?: boolean
|
|
5
|
+
}
|
|
6
|
+
export declare class MemoryFeatureFlagDriver implements FeatureDriver {
|
|
7
|
+
constructor(options?: MemoryFeatureFlagDriverOptions);
|
|
8
|
+
get(name: string, scopeKey: string): Promise<FeatureValue | undefined>;
|
|
9
|
+
set(name: string, scopeKey: string, value: FeatureValue): Promise<void>;
|
|
10
|
+
delete(name: string, scopeKey: string): Promise<void>;
|
|
11
|
+
deleteForAllScopes(names?: readonly string[]): Promise<void>;
|
|
12
|
+
clear(): Promise<void>;
|
|
13
|
+
stored(scopeKey: string): Promise<Record<string, FeatureValue>>;
|
|
14
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class FeatureFlagError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
export declare class FeatureNotDefinedError extends FeatureFlagError {
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(feature: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class InvalidFeatureValueError extends FeatureFlagError {
|
|
9
|
+
name: string;
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class InvalidFeatureScopeError extends FeatureFlagError {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class FeatureFlagStoreError extends FeatureFlagError {
|
|
16
|
+
name: string;
|
|
17
|
+
constructor(message: string, options?: ErrorOptions);
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare function configuredDriver(config: FeatureFlagsConfig, dialect: string | undefined): Promise<FeatureDriver>;
|
|
2
|
+
/**
|
|
3
|
+
* Global, configuration-aware feature facade.
|
|
4
|
+
*
|
|
5
|
+
* The config and database imports stay lazy so defining flags at application
|
|
6
|
+
* boot cannot create a config/database module cycle.
|
|
7
|
+
*/
|
|
8
|
+
export declare const Feature: FeatureFlagManager;
|
|
9
|
+
export declare const featureFlags: unknown;
|
|
10
|
+
export * from './drivers/index';
|
|
11
|
+
export * from './errors';
|
|
12
|
+
export * from './manager';
|
|
13
|
+
export * from './schema';
|
|
14
|
+
export * from './scope';
|
|
15
|
+
export * from './strategies';
|
|
16
|
+
export * from './types';
|
|
17
|
+
export * from './value';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var T=import.meta.require;class B extends Error{name="FeatureFlagError"}class q extends B{name="FeatureNotDefinedError";constructor(P){super(`Feature flag '${P}' has not been defined.`)}}class Y extends B{name="InvalidFeatureValueError";constructor(P){super(`Feature values must be JSON-safe. ${P}`)}}class X extends B{name="InvalidFeatureScopeError"}class Z extends B{name="FeatureFlagStoreError";constructor(P,M){super(P,M)}}function V(P){if(P===void 0)throw new Y("Undefined is not a valid feature value.");if(typeof P==="number"&&!Number.isFinite(P))throw new Y("NaN and Infinity are not valid feature values.");if(typeof P==="bigint"||typeof P==="function"||typeof P==="symbol")throw new Y(`Values of type '${typeof P}' are not supported.`);try{let M=JSON.stringify(P);if(M===void 0)throw new Y("The value cannot be serialized.");return JSON.parse(M)}catch(M){if(M instanceof Y)throw M;throw new Y(M instanceof Error?M.message:String(M))}}function O(P="feature_flags"){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(P))throw new Z(`Invalid feature flag table name '${P}'.`);return P}function F(P={}){let M=O(P.table),$=P.dialect??"sqlite";if($==="mysql")return[`CREATE TABLE IF NOT EXISTS ${M} (name VARCHAR(191) NOT NULL,scope VARCHAR(512) NOT NULL,value TEXT NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY ${M}_name_scope_unique (name, scope),KEY ${M}_scope_index (scope))`];let J=$==="postgres"?"TIMESTAMPTZ":"DATETIME";return[`CREATE TABLE IF NOT EXISTS ${M} (name VARCHAR(191) NOT NULL,scope VARCHAR(512) NOT NULL,value TEXT NOT NULL,created_at ${J} DEFAULT CURRENT_TIMESTAMP,updated_at ${J} DEFAULT CURRENT_TIMESTAMP,UNIQUE (name, scope))`,`CREATE INDEX IF NOT EXISTS ${M}_scope_index ON ${M}(scope)`]}async function g(P,M){if(!P.unsafe)throw new Z("The database client does not support schema provisioning via unsafe SQL.");let $=P.unsafe(M);if($&&typeof $.execute==="function")await $.execute();else await $}async function D(P,M={}){for(let $ of F(M))await g(P,$)}function f(P){return JSON.stringify(V(P))}function S(P){try{return V(JSON.parse(P.value))}catch(M){throw new Z(`Stored value for feature '${P.name}' is invalid JSON.`,{cause:M})}}function K(P){let M=P,$=M?.message??"";return M?.code==="23505"||M?.code==="ER_DUP_ENTRY"||/unique constraint|duplicate entry/i.test($)}class C{database;table;ready;constructor(P,M={}){this.database=P;this.table=O(M.table),this.ready=M.autoCreate?D(P,{table:this.table,dialect:M.dialect}):Promise.resolve()}async get(P,M){await this.ready;let $=await this.database.selectFrom(this.table).where("name","=",P).where("scope","=",M).select(["name","value"]).executeTakeFirst();return $?S($):void 0}async set(P,M,$){await this.ready;let J=f($),L=new Date().toISOString();if(await this.database.selectFrom(this.table).where("name","=",P).where("scope","=",M).select(["name"]).executeTakeFirst()){await this.update(P,M,J,L);return}try{await this.database.insertInto(this.table).values({name:P,scope:M,value:J,created_at:L,updated_at:L}).execute()}catch(W){if(!K(W))throw W;await this.update(P,M,J,L)}}async update(P,M,$,J){await this.database.updateTable(this.table).set({value:$,updated_at:J}).where("name","=",P).where("scope","=",M).execute()}async delete(P,M){await this.ready,await this.database.deleteFrom(this.table).where("name","=",P).where("scope","=",M).execute()}async deleteForAllScopes(P){if(await this.ready,!P){await this.clear();return}for(let M of new Set(P))await this.database.deleteFrom(this.table).where("name","=",M).execute()}async clear(){await this.ready,await this.database.deleteFrom(this.table).execute()}async stored(P){await this.ready;let M=await this.database.selectFrom(this.table).where("scope","=",P).select(["name","value"]).execute(),$={};for(let J of M)Object.defineProperty($,J.name,{value:S(J),enumerable:!0,configurable:!0,writable:!0});return $}}function a(P,M){return new C(P,M)}class R{values=new Map;cloneValues;constructor(P={}){this.cloneValues=P.cloneValues??!0}clone(P){return this.cloneValues?V(P):P}async get(P,M){let $=this.values.get(M)?.get(P);return $===void 0?void 0:this.clone($)}async set(P,M,$){let J=this.values.get(M);if(!J)J=new Map,this.values.set(M,J);J.set(P,this.clone($))}async delete(P,M){let $=this.values.get(M);if($?.delete(P),$?.size===0)this.values.delete(M)}async deleteForAllScopes(P){if(!P){this.values.clear();return}let M=new Set(P);for(let[$,J]of this.values){for(let L of M)J.delete(L);if(J.size===0)this.values.delete($)}}async clear(){this.values.clear()}async stored(P){let M={};for(let[$,J]of this.values.get(P)??[])Object.defineProperty(M,$,{value:this.clone(J),enumerable:!0,configurable:!0,writable:!0});return M}}function MP(P){return new R(P)}import{createHash as b}from"crypto";function U(P){if(new TextEncoder().encode(P).byteLength<=512)return P;return`sha256:${b("sha256").update(P).digest("hex")}`}function A(P,M){if(P===null||typeof P!=="object"){if(typeof P==="bigint")return{$bigint:P.toString()};if(typeof P==="symbol"||typeof P==="function"||P===void 0)throw new X(`Feature scope contains unsupported value '${typeof P}'.`);if(typeof P==="number"&&!Number.isFinite(P))throw new X("Feature scope cannot contain NaN or Infinity.");return P}if(M.has(P))throw new X("Feature scope cannot contain circular references.");M.add(P);try{if(P instanceof Date)return{$date:P.toISOString()};if(Array.isArray(P))return P.map((L)=>A(L,M));let $=Object.getPrototypeOf(P);if($!==Object.prototype&&$!==null)throw new X("Feature scope objects must be plain objects or expose featureFlagScope.");let J={};for(let L of Object.keys(P).sort())J[L]=A(P[L],M);return J}finally{M.delete(P)}}function y(P){return typeof P.featureFlagScope==="function"?P.featureFlagScope():P.featureFlagScope}function d(P){return(typeof P.featureFlagType==="function"?P.featureFlagType():P.featureFlagType)?.trim()||P.constructor?.name||"object"}function u(P,M){let $=typeof P.featureFlagType==="function"?P.featureFlagType():P.featureFlagType;if(typeof $==="string"&&$.trim())return $.trim();let J=P._definition;if(J&&typeof J==="object"){let L=J.name;if(typeof L==="string"&&L.trim())return L.trim()}return M.constructor?.name||"object"}function k(P){if(P===null||P===void 0)return"global";if(typeof P==="string")return U(`string:${P}`);if(typeof P==="number"){if(!Number.isFinite(P))throw new X("Feature scope cannot be NaN or Infinity.");return`number:${P}`}if(typeof P==="bigint")return`bigint:${P}`;if(typeof P==="boolean")return`boolean:${P}`;if(typeof P==="symbol"||typeof P==="function")throw new X(`Feature scope of type '${typeof P}' is not supported.`);let M=P;if("featureFlagScope"in M){let $=M,J=y($);if(typeof J!=="string"&&typeof J!=="number"||String(J).length===0)throw new X("featureFlagScope must resolve to a non-empty string or number.");return U(`model:${d($)}:${String(J)}`)}if("id"in M&&(typeof M.id==="string"||typeof M.id==="number")){let $=u(M,P);return U(`model:${$}:${String(M.id)}`)}return U(`object:${JSON.stringify(A(P,new WeakSet))}`)}function _(P){if(typeof P!=="string"||P.trim().length===0)throw TypeError("Feature flag names must be non-empty strings.");if(P!==P.trim())throw TypeError("Feature flag names cannot start or end with whitespace.");if(new TextEncoder().encode(P).byteLength>191)throw TypeError("Feature flag names cannot exceed 191 UTF-8 bytes.");return P}function E(P,M){return JSON.stringify([P,M])}function h(P){return typeof P==="function"?P:V(P)}function N(P,M,$){Object.defineProperty(P,M,{value:$,enumerable:!0,configurable:!0,writable:!0})}class I{manager;scope;constructor(P,M){this.manager=P;this.scope=M}value(P){return this.manager.value(P,this.scope)}values(P){return this.manager.values(P,this.scope)}all(){return this.manager.all(this.scope)}active(P){return this.manager.active(P,this.scope)}inactive(P){return this.manager.inactive(P,this.scope)}activate(P,M=!0){return this.manager.activate(P,M,this.scope)}deactivate(P){return this.manager.deactivate(P,this.scope)}forget(P){return this.manager.forget(P,this.scope)}when(P,M,$){return this.manager.when(P,M,$,this.scope)}unless(P,M,$){return this.manager.unless(P,M,$,this.scope)}}class x{definitions=new Map;pending=new Map;listeners=new Set;resolveScope;driverSource;driverPromise;missingBehavior;constructor(P={}){this.driverSource=P.driver??new R,this.missingBehavior=P.missing??"false",this.resolveScope=P.scopeResolver??k}use(P){return this.driverSource=P,this.driverPromise=void 0,this.pending.clear(),this}missing(P){return this.missingBehavior=P,this}define(P,M){if(typeof P==="string"){if(M===void 0)throw TypeError(`A definition is required for feature '${P}'.`);return this.definitions.set(_(P),h(M)),this}for(let[$,J]of Object.entries(P))this.definitions.set(_($),h(J));return this}defined(P){return this.definitions.has(_(P))}definedNames(){return[...this.definitions.keys()]}clearDefinitions(){return this.definitions.clear(),this}for(P){return new I(this,P)}async value(P,M=null){let $=_(P),J=this.resolveScope(M),L=E($,J),Q=this.pending.get(L);if(Q)return Q;let W=this.evaluate($,M,J);this.pending.set(L,W);try{return await W}finally{if(this.pending.get(L)===W)this.pending.delete(L)}}async evaluate(P,M,$){let J=await this.driver(),L=await J.get(P,$);if(L!==void 0){let G=V(L);return await this.notify({name:P,scope:M,scopeKey:$,value:G,source:"stored"}),G}let Q=this.definitions.get(P);if(Q===void 0){if(this.missingBehavior==="throw")throw new q(P);let G=!1;return await this.notify({name:P,scope:M,scopeKey:$,value:G,source:"missing"}),G}let W=typeof Q==="function"?await Q(M,{name:P,scope:M,scopeKey:$}):Q,H=V(W);return await J.set(P,$,H),await this.notify({name:P,scope:M,scopeKey:$,value:H,source:"resolver"}),H}async values(P,M=null){let $=[...new Set(P.map(_))],J=await Promise.all($.map(async(L)=>[L,await this.value(L,M)]));return Object.fromEntries(J)}async all(P=null){let M=this.resolveScope(P),$=await(await this.driver()).stored(M),J={};for(let[L,Q]of Object.entries($))N(J,L,V(Q));for(let L of this.definitions.keys())if(!Object.hasOwn(J,L))N(J,L,await this.value(L,P));return J}async active(P,M=null){return Boolean(await this.value(P,M))}async inactive(P,M=null){return!await this.active(P,M)}async activate(P,M=!0,$=null){let J=_(P),L=this.resolveScope($);await this.awaitPending(J,L),await(await this.driver()).set(J,L,V(M))}deactivate(P,M=null){return this.activate(P,!1,M)}async forget(P,M=null){let $=_(P),J=this.resolveScope(M);await this.awaitPending($,J),await(await this.driver()).delete($,J)}async purge(P){let M=P===void 0?void 0:(Array.isArray(P)?P:[P]).map(_);await Promise.allSettled(this.pending.values()),await(await this.driver()).deleteForAllScopes(M)}async flush(){await Promise.allSettled(this.pending.values()),await(await this.driver()).clear()}async when(P,M,$,J=null){let L=await this.value(P,J);return Boolean(L)?M(L):$?.(L)}async unless(P,M,$,J=null){let L=await this.value(P,J);return Boolean(L)?$?.(L):M(L)}onEvaluated(P){return this.listeners.add(P),()=>this.listeners.delete(P)}async notify(P){await Promise.allSettled([...this.listeners].map((M)=>Promise.resolve().then(()=>M(P))))}async awaitPending(P,M){let $=this.pending.get(E(P,M));if($)try{await $}catch{}}driver(){if(!this.driverPromise){let P=Promise.resolve(typeof this.driverSource==="function"?this.driverSource():this.driverSource);this.driverPromise=P,P.catch(()=>{if(this.driverPromise===P)this.driverPromise=void 0})}return this.driverPromise}}function ZP(P){return new x(P)}function v(P){if(!Number.isFinite(P)||P<0||P>100)throw RangeError(`Percentage must be between 0 and 100. Received ${P}.`)}function j(P){let M=new TextEncoder().encode(P),$=2166136261;for(let J of M)$^=J,$=Math.imul($,16777619);return($>>>0)/4294967296*100}function GP(P){return v(P),(M,$)=>j(`${$.name}\x00${$.scopeKey}`)<P}function HP(P){let M=Object.entries(P),$=M[M.length-1];if(!$)throw RangeError("At least one feature variant is required.");let J=0;for(let[L,Q]of M){if(!Number.isFinite(Q)||Q<=0)throw RangeError(`Variant '${L}' must have a positive, finite weight.`);J+=Q}return(L,Q)=>{let W=j(`${Q.name}\x00${Q.scopeKey}`)/100*J,H=0;for(let[G,z]of M)if(H+=z,W<H)return G;return $[0]}}async function m(P,M){if(P.default==="database"){if(M!=="sqlite"&&M!=="mysql"&&M!=="singlestore"&&M!=="postgres")throw new Z(`The feature flag database driver requires a SQL database. Received '${M??"undefined"}'.`);let{db:$}=await import("@stacksjs/database");return new C($,{...P.drivers?.database,dialect:M==="mysql"||M==="singlestore"?"mysql":M==="postgres"?"postgres":"sqlite"})}return new R(P.drivers?.memory)}var w=new x({driver:async()=>{let{awaitConfig:P}=await import("@stacksjs/config"),M=await P();return w.missing(M.featureFlags.missing??"false"),m(M.featureFlags,M.database.default)}}),qP=w;export{HP as variants,GP as percentage,V as normalizeFeatureValue,k as featureScopeKey,qP as featureFlags,O as featureFlagTableName,F as featureFlagMigrationSql,j as featureBucket,D as ensureFeatureFlagTable,MP as createMemoryFeatureFlagDriver,ZP as createFeatureFlags,a as createDatabaseFeatureFlagDriver,I as ScopedFeatureFlags,R as MemoryFeatureFlagDriver,Y as InvalidFeatureValueError,X as InvalidFeatureScopeError,q as FeatureNotDefinedError,Z as FeatureFlagStoreError,x as FeatureFlagManager,B as FeatureFlagError,w as Feature,C as DatabaseFeatureFlagDriver};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FeatureDefinition, FeatureDriver, FeatureDriverFactory, FeatureEvaluation, FeatureFlagManagerOptions, FeatureScope, FeatureValue } from './types';
|
|
2
|
+
export declare function createFeatureFlags(options?: FeatureFlagManagerOptions): FeatureFlagManager;
|
|
3
|
+
declare type EvaluationListener = (evaluation: FeatureEvaluation) => void | Promise<void>;
|
|
4
|
+
export declare class ScopedFeatureFlags<Scope = FeatureScope> {
|
|
5
|
+
readonly scope: Scope;
|
|
6
|
+
constructor(manager: FeatureFlagManager, scope: Scope);
|
|
7
|
+
value(name: string): Promise<FeatureValue>;
|
|
8
|
+
values(names: readonly string[]): Promise<Record<string, FeatureValue>>;
|
|
9
|
+
all(): Promise<Record<string, FeatureValue>>;
|
|
10
|
+
active(name: string): Promise<boolean>;
|
|
11
|
+
inactive(name: string): Promise<boolean>;
|
|
12
|
+
activate(name: string, value?: FeatureValue): Promise<void>;
|
|
13
|
+
deactivate(name: string): Promise<void>;
|
|
14
|
+
forget(name: string): Promise<void>;
|
|
15
|
+
when<Active, Inactive = undefined>(name: string, onActive: (value: FeatureValue) => Active | Promise<Active>, onInactive?: (value: FeatureValue) => Inactive | Promise<Inactive>): Promise<Active | Inactive | undefined>;
|
|
16
|
+
unless<Inactive, Active = undefined>(name: string, onInactive: (value: FeatureValue) => Inactive | Promise<Inactive>, onActive?: (value: FeatureValue) => Active | Promise<Active>): Promise<Inactive | Active | undefined>;
|
|
17
|
+
}
|
|
18
|
+
export declare class FeatureFlagManager {
|
|
19
|
+
constructor(options?: FeatureFlagManagerOptions);
|
|
20
|
+
use(driver: FeatureDriver | FeatureDriverFactory): this;
|
|
21
|
+
missing(behavior: 'false' | 'throw'): this;
|
|
22
|
+
define<Scope = FeatureScope>(name: string, definition: FeatureDefinition<Scope>): this;
|
|
23
|
+
define(definitions: Record<string, FeatureDefinition>): this;
|
|
24
|
+
define<Scope = FeatureScope>(nameOrDefinitions: string | Record<string, FeatureDefinition>, definition?: FeatureDefinition<Scope>): this;
|
|
25
|
+
defined(name: string): boolean;
|
|
26
|
+
definedNames(): string[];
|
|
27
|
+
clearDefinitions(): this;
|
|
28
|
+
for<Scope>(scope: Scope): ScopedFeatureFlags<Scope>;
|
|
29
|
+
value(name: string, scope?: FeatureScope): Promise<FeatureValue>;
|
|
30
|
+
values(names: readonly string[], scope?: FeatureScope): Promise<Record<string, FeatureValue>>;
|
|
31
|
+
all(scope?: FeatureScope): Promise<Record<string, FeatureValue>>;
|
|
32
|
+
active(name: string, scope?: FeatureScope): Promise<boolean>;
|
|
33
|
+
inactive(name: string, scope?: FeatureScope): Promise<boolean>;
|
|
34
|
+
activate(name: string, value?: FeatureValue, scope?: FeatureScope): Promise<void>;
|
|
35
|
+
deactivate(name: string, scope?: FeatureScope): Promise<void>;
|
|
36
|
+
forget(name: string, scope?: FeatureScope): Promise<void>;
|
|
37
|
+
purge(names?: string | readonly string[]): Promise<void>;
|
|
38
|
+
flush(): Promise<void>;
|
|
39
|
+
when<Active, Inactive = undefined>(name: string, onActive: (value: FeatureValue) => Active | Promise<Active>, onInactive?: (value: FeatureValue) => Inactive | Promise<Inactive>, scope?: FeatureScope): Promise<Active | Inactive | undefined>;
|
|
40
|
+
unless<Inactive, Active = undefined>(name: string, onInactive: (value: FeatureValue) => Inactive | Promise<Inactive>, onActive?: (value: FeatureValue) => Active | Promise<Active>, scope?: FeatureScope): Promise<Inactive | Active | undefined>;
|
|
41
|
+
onEvaluated(listener: EvaluationListener): () => void;
|
|
42
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeatureDatabaseClient } from './types';
|
|
2
|
+
export declare function featureFlagTableName(table?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Return the package-owned schema for publishing or explicit provisioning.
|
|
5
|
+
* No application migration file is written implicitly.
|
|
6
|
+
*/
|
|
7
|
+
export declare function featureFlagMigrationSql(options?: FeatureFlagSchemaOptions): string[];
|
|
8
|
+
/** Explicitly provision the feature flag table using the package-owned schema. */
|
|
9
|
+
export declare function ensureFeatureFlagTable(database: FeatureDatabaseClient, options?: FeatureFlagSchemaOptions): Promise<void>;
|
|
10
|
+
export declare interface FeatureFlagSchemaOptions {
|
|
11
|
+
table?: string
|
|
12
|
+
dialect?: FeatureFlagSqlDialect
|
|
13
|
+
}
|
|
14
|
+
export type FeatureFlagSqlDialect = 'sqlite' | 'mysql' | 'postgres';
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FeatureResolver } from './types';
|
|
2
|
+
/** Stable FNV-1a hash normalized to the half-open range [0, 100). */
|
|
3
|
+
export declare function featureBucket(input: string): number;
|
|
4
|
+
/** Deterministically activate a percentage of scopes for a flag. */
|
|
5
|
+
export declare function percentage(percent: number): FeatureResolver;
|
|
6
|
+
/** Deterministically assign every scope to one weighted variant. */
|
|
7
|
+
export declare function variants<const Name extends string>(weights: Record<Name, number>): FeatureResolver<unknown>;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare interface FeatureScopeKeyProvider {
|
|
2
|
+
featureFlagScope: string | number | (() => string | number)
|
|
3
|
+
featureFlagType?: string | (() => string)
|
|
4
|
+
}
|
|
5
|
+
export declare interface FeatureContext<Scope = FeatureScope> {
|
|
6
|
+
name: string
|
|
7
|
+
scope: Scope
|
|
8
|
+
scopeKey: string
|
|
9
|
+
}
|
|
10
|
+
export declare interface FeatureEvaluation<Scope = FeatureScope> extends FeatureContext<Scope> {
|
|
11
|
+
value: FeatureValue
|
|
12
|
+
source: FeatureEvaluationSource
|
|
13
|
+
}
|
|
14
|
+
export declare interface FeatureDriver {
|
|
15
|
+
get: (name: string, scopeKey: string) => Promise<FeatureValue | undefined>
|
|
16
|
+
set: (name: string, scopeKey: string, value: FeatureValue) => Promise<void>
|
|
17
|
+
delete: (name: string, scopeKey: string) => Promise<void>
|
|
18
|
+
deleteForAllScopes: (names?: readonly string[]) => Promise<void>
|
|
19
|
+
clear: () => Promise<void>
|
|
20
|
+
stored: (scopeKey: string) => Promise<Record<string, FeatureValue>>
|
|
21
|
+
}
|
|
22
|
+
export declare interface FeatureFlagManagerOptions {
|
|
23
|
+
driver?: FeatureDriver | FeatureDriverFactory
|
|
24
|
+
missing?: 'false' | 'throw'
|
|
25
|
+
scopeResolver?: (scope: FeatureScope) => string
|
|
26
|
+
}
|
|
27
|
+
export declare interface DatabaseFeatureFlagDriverOptions {
|
|
28
|
+
table?: string
|
|
29
|
+
autoCreate?: boolean
|
|
30
|
+
dialect?: 'sqlite' | 'mysql' | 'postgres'
|
|
31
|
+
}
|
|
32
|
+
export declare interface FeatureDatabaseClient {
|
|
33
|
+
selectFrom: (table: string) => any
|
|
34
|
+
insertInto: (table: string) => any
|
|
35
|
+
updateTable: (table: string) => any
|
|
36
|
+
deleteFrom: (table: string) => any
|
|
37
|
+
unsafe?: (sql: string) => any
|
|
38
|
+
}
|
|
39
|
+
export type FeatureScalar = boolean | string | number | null;
|
|
40
|
+
export type FeatureValue = | FeatureScalar
|
|
41
|
+
| { [key: string]: FeatureValue }
|
|
42
|
+
| FeatureValue[];
|
|
43
|
+
export type FeatureScope = unknown;
|
|
44
|
+
export type FeatureResolver<Scope = FeatureScope> = (
|
|
45
|
+
scope: Scope,
|
|
46
|
+
context: FeatureContext<Scope>,
|
|
47
|
+
) => FeatureValue | Promise<FeatureValue>;
|
|
48
|
+
export type FeatureDefinition<Scope = FeatureScope> = FeatureValue | FeatureResolver<Scope>;
|
|
49
|
+
export type FeatureEvaluationSource = 'stored' | 'resolver' | 'missing';
|
|
50
|
+
export type FeatureDriverFactory = () => FeatureDriver | Promise<FeatureDriver>;
|
package/dist/value.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stacksjs/feature-flags",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"version": "0.70.90",
|
|
6
|
+
"description": "Typed, scoped feature flags for Stacks applications.",
|
|
7
|
+
"author": "Chris Breuer",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"Chris Breuer <chris@stacksjs.com>"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"funding": "https://github.com/sponsors/chrisbbreuer",
|
|
13
|
+
"homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/feature-flags#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
17
|
+
"directory": "./storage/framework/core/feature-flags"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/stacksjs/stacks/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"feature-flags",
|
|
24
|
+
"rollout",
|
|
25
|
+
"experiments",
|
|
26
|
+
"stacks"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"development": "./src/index.ts",
|
|
32
|
+
"bun": "./dist/index.js",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./*": {
|
|
37
|
+
"development": "./src/*",
|
|
38
|
+
"bun": "./dist/*",
|
|
39
|
+
"import": "./dist/*",
|
|
40
|
+
"default": "./dist/*"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"module": "dist/index.js",
|
|
44
|
+
"types": "dist/index.d.ts",
|
|
45
|
+
"files": [
|
|
46
|
+
"README.md",
|
|
47
|
+
"dist"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "bun build.ts",
|
|
51
|
+
"test": "bun test",
|
|
52
|
+
"typecheck": "bun tsc --noEmit",
|
|
53
|
+
"prepublishOnly": "bun run build"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@stacksjs/config": "0.70.90",
|
|
57
|
+
"@stacksjs/database": "0.70.90",
|
|
58
|
+
"@stacksjs/query-builder": "0.70.90",
|
|
59
|
+
"better-dx": "^0.2.16"
|
|
60
|
+
}
|
|
61
|
+
}
|