@tehw0lf/yaft 0.0.15 → 0.0.17

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.
Files changed (51) hide show
  1. package/FeatureToggle.d.ts +20 -0
  2. package/FeatureToggle.js +72 -0
  3. package/README.md +25 -3
  4. package/evaluate.d.ts +42 -0
  5. package/evaluate.js +102 -0
  6. package/examples/ApiServiceBooleanProvider.d.ts +11 -0
  7. package/examples/ApiServiceBooleanProvider.js +85 -0
  8. package/examples/ApiServiceFeatureProvider.d.ts +17 -0
  9. package/examples/ApiServiceFeatureProvider.js +54 -0
  10. package/examples/LocalStorageBooleanProvider.d.ts +7 -0
  11. package/examples/LocalStorageBooleanProvider.js +23 -0
  12. package/examples/LocalStorageFeatureProvider.d.ts +14 -0
  13. package/examples/LocalStorageFeatureProvider.js +31 -0
  14. package/index.d.ts +3 -0
  15. package/index.js +14 -0
  16. package/mapping.d.ts +35 -0
  17. package/mapping.js +98 -0
  18. package/package.json +21 -9
  19. package/.github/workflows/build.yml +0 -28
  20. package/.github/workflows/security-scan.yml +0 -79
  21. package/CLAUDE.md +0 -151
  22. package/LICENSE +0 -21
  23. package/conformance.lock +0 -2
  24. package/jest.config.js +0 -16
  25. package/scripts/fetch-conformance.sh +0 -67
  26. package/src/FeatureToggle.ts +0 -95
  27. package/src/evaluate.ts +0 -122
  28. package/src/examples/ApiServiceBooleanProvider.ts +0 -82
  29. package/src/examples/ApiServiceFeatureProvider.ts +0 -52
  30. package/src/examples/LocalStorageBooleanProvider.ts +0 -25
  31. package/src/examples/LocalStorageFeatureProvider.ts +0 -31
  32. package/src/index.ts +0 -8
  33. package/src/mapping.ts +0 -92
  34. package/src/test/conformance-adapter/cases.ts +0 -104
  35. package/src/test/conformance-adapter/decorator.spec.ts +0 -376
  36. package/src/test/conformance-adapter/evaluation.spec.ts +0 -31
  37. package/src/test/conformance-adapter/mapping.spec.ts +0 -79
  38. package/src/test/decorator-behavior.spec.ts +0 -359
  39. package/src/test/error-handling.spec.ts +0 -396
  40. package/src/test/evaluate.spec.ts +0 -260
  41. package/src/test/feature-data.spec.ts +0 -102
  42. package/src/test/injectable-clock.spec.ts +0 -112
  43. package/src/test/integration.spec.ts +0 -472
  44. package/src/test/providers.spec.ts +0 -218
  45. package/src/test/test-boolean.json +0 -1
  46. package/src/test/test-feature.json +0 -20
  47. package/src/test/test-setup.ts +0 -2
  48. package/src/test/time-based-logic.spec.ts +0 -169
  49. package/src/test/yaft.demo.spec.ts +0 -53
  50. package/tsconfig.json +0 -14
  51. package/tsconfig.spec.json +0 -10
package/mapping.js ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normaliseFeature = normaliseFeature;
4
+ exports.normaliseCollection = normaliseCollection;
5
+ exports.normaliseBooleans = normaliseBooleans;
6
+ /**
7
+ * Reads a field by presence, not by truthiness.
8
+ *
9
+ * The obvious `raw.value || raw.Value` is wrong: a present but empty value
10
+ * falls through to the other spelling, so a feature stored as `""` reads as
11
+ * whatever the capitalised field holds. An off feature then reports on. The
12
+ * same trap applies to `tags: []`.
13
+ *
14
+ * Backends from 0.2.0 on send only the lowercase spelling; the capitalised one
15
+ * is read because instances before that are still around.
16
+ */
17
+ function field(raw, lower, upper) {
18
+ if (lower in raw)
19
+ return raw[lower];
20
+ if (upper in raw)
21
+ return raw[upper];
22
+ return undefined;
23
+ }
24
+ /**
25
+ * Normalises a date field. The backend sends `null` for an unset bound and
26
+ * local fixtures use `""`; both mean "no bound", and `evaluate` ignores either.
27
+ */
28
+ function date(value) {
29
+ return typeof value === 'string' ? value : '';
30
+ }
31
+ /** Normalises one entry into a `Feature`, whichever spelling it arrived in. */
32
+ function normaliseFeature(raw) {
33
+ const tags = field(raw, 'tags', 'Tags');
34
+ return {
35
+ key: String(field(raw, 'key', 'Key') ?? ''),
36
+ value: String(field(raw, 'value', 'Value') ?? ''),
37
+ activeAt: date(field(raw, 'activeAt', 'ActiveAt')),
38
+ disabledAt: date(field(raw, 'disabledAt', 'DisabledAt')),
39
+ // Filtered rather than asserted: `as string[]` is a compile-time claim
40
+ // that a backend sending a mixed array would quietly break, handing
41
+ // callers a non-string through a field typed as string.
42
+ tags: Array.isArray(tags)
43
+ ? tags.filter((tag) => typeof tag === 'string')
44
+ : [],
45
+ };
46
+ }
47
+ /**
48
+ * Normalises a whole response into features keyed by their key.
49
+ *
50
+ * Three envelopes are accepted, because the backend uses all three:
51
+ *
52
+ * - `{ "toggles": [...] }` for a UUID group;
53
+ * - `{ "value": [...] }`, the same thing under a different name;
54
+ * - a flat object for a single toggle.
55
+ *
56
+ * An entry without a usable key is skipped rather than stored under `""`,
57
+ * where `isEnabled("")` could reach it.
58
+ */
59
+ function normaliseCollection(response) {
60
+ if (response === null || typeof response !== 'object')
61
+ return {};
62
+ const body = response;
63
+ const collection = Array.isArray(body['toggles'])
64
+ ? body['toggles']
65
+ : Array.isArray(body['value'])
66
+ ? body['value']
67
+ : undefined;
68
+ // A single toggle comes back flat, not wrapped. Older code only handled the
69
+ // collections and dropped this shape entirely.
70
+ const entries = collection ?? [body];
71
+ const data = {};
72
+ for (const entry of entries) {
73
+ if (entry === null || typeof entry !== 'object')
74
+ continue;
75
+ const feature = normaliseFeature(entry);
76
+ if (feature.key !== '')
77
+ data[feature.key] = feature;
78
+ }
79
+ return data;
80
+ }
81
+ /**
82
+ * Normalises a boolean-shape payload, `{ "myToggle": true }`.
83
+ *
84
+ * Only real booleans are kept (R29). Anything else is dropped, so its key
85
+ * reads as missing and therefore off. Keeping it and testing its truthiness
86
+ * would turn `"false"` on, since a non-empty string is truthy.
87
+ */
88
+ function normaliseBooleans(response) {
89
+ if (response === null || typeof response !== 'object' || Array.isArray(response)) {
90
+ return {};
91
+ }
92
+ const data = {};
93
+ for (const [key, value] of Object.entries(response)) {
94
+ if (typeof value === 'boolean')
95
+ data[key] = value;
96
+ }
97
+ return data;
98
+ }
package/package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "name": "@tehw0lf/yaft",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
5
5
  "type": "commonjs",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./examples/*": {
14
+ "types": "./examples/*.d.ts",
15
+ "default": "./examples/*.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
6
19
  "scripts": {
7
20
  "build": "tsc && cp package.json README.md logo.svg dist/yaft/ && npm pkg delete devDependencies --prefix dist/yaft",
8
21
  "test": "npx jest",
@@ -28,13 +41,12 @@
28
41
  "dependencies": {
29
42
  "reflect-metadata": "0.2.2"
30
43
  },
31
- "devDependencies": {
32
- "@types/jest": "30.0.0",
33
- "@types/node": "24.1.0",
34
- "axios": "1.18.0",
35
- "jest": "30.4.2",
36
- "ts-jest": "29.4.11",
37
- "ts-node": "10.9.2",
38
- "typescript": "6.0.3"
44
+ "peerDependencies": {
45
+ "axios": ">=1.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "axios": {
49
+ "optional": true
50
+ }
39
51
  }
40
52
  }
@@ -1,28 +0,0 @@
1
- name: build and publish pipeline
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
-
9
- jobs:
10
- build:
11
- name: external workflow
12
- uses: tehw0lf/workflows/.github/workflows/build-test-publish.yml@main
13
- permissions:
14
- actions: write
15
- attestations: write
16
- contents: write
17
- packages: write
18
- id-token: write
19
- security-events: write
20
- pull-requests: write
21
- with:
22
- tool: "npm"
23
- test: "run test"
24
- build_branch: "run build"
25
- build_main: "run build"
26
- artifact_path: "dist"
27
- library_path: "dist"
28
- npm_audit_omit_dev: true
@@ -1,79 +0,0 @@
1
- # Security Monitoring Workflow
2
- #
3
- # This workflow performs security scanning using only the security-specific
4
- # reusable workflows, without building or publishing.
5
- #
6
- # Schedule: Runs daily at 2:00 AM UTC to catch newly disclosed vulnerabilities
7
- # Manual Trigger: Can be triggered via GitHub Actions UI or gh CLI
8
- #
9
- # Security Checks:
10
- # - Source code scanning: Semgrep (OWASP Top 10, security-audit, CI), npm audit
11
- # - Artifact scanning: Trivy on dist/ artifacts (requires prior build)
12
- #
13
- name: Security Monitoring
14
-
15
- on:
16
- schedule:
17
- # Run daily at 2:00 AM UTC
18
- - cron: "0 2 * * *"
19
- workflow_dispatch:
20
- # Allow manual triggering
21
-
22
- permissions:
23
- contents: read
24
- security-events: write
25
- actions: read
26
- pull-requests: write
27
-
28
- jobs:
29
- # Scan source code for security vulnerabilities
30
- scan_source:
31
- name: Scan Source Code
32
- uses: tehw0lf/workflows/.github/workflows/security-scan-source.yml@main
33
- permissions:
34
- contents: write
35
- security-events: write
36
- actions: read
37
- pull-requests: write
38
- with:
39
- tool: "npm"
40
- semgrep_rules: "auto p/security-audit p/owasp-top-ten p/ci p/nodejs p/typescript"
41
- npm_audit_omit_dev: true
42
- npm_audit_severity_threshold: "high"
43
-
44
- # Download artifacts from latest CI build
45
- # Note: Reuses artifacts from the main CI workflow instead of rebuilding
46
- download_artifacts:
47
- name: Download Build Artifacts
48
- runs-on: ubuntu-latest
49
-
50
- steps:
51
- - name: Download latest dist artifacts from CI
52
- uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
53
- with:
54
- workflow: build.yml
55
- name: dist
56
- path: dist/
57
- if_no_artifact_found: warn
58
-
59
- - name: Upload artifacts for scanning
60
- if: hashFiles('dist/**/*') != ''
61
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
62
- with:
63
- name: build
64
- path: dist/
65
- retention-days: 1
66
-
67
- # Scan built artifacts for vulnerabilities
68
- scan_artifacts:
69
- name: Scan Artifacts
70
- needs: download_artifacts
71
- if: needs.download_artifacts.result == 'success'
72
- uses: tehw0lf/workflows/.github/workflows/security-scan-artifacts.yml@main
73
- permissions:
74
- contents: read
75
- security-events: write
76
- actions: read
77
- with:
78
- tool: "npm"
79
- artifact_path: "dist"
package/CLAUDE.md DELETED
@@ -1,151 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- YaFT (Yet another Feature Toggle) is a TypeScript library that provides decorator-based feature toggles for classes and methods. It integrates with backend services (Go/PostgreSQL by default) or local data sources to enable/disable features at runtime.
8
-
9
- ## Development Commands
10
-
11
- ### Building
12
- ```bash
13
- npm run build # Compile TypeScript and prepare package for publishing
14
- ```
15
-
16
- ### Testing
17
- ```bash
18
- npm test # Run Jest tests with ts-jest transform
19
- npx jest # Alternative test command
20
- npx jest --watch # Run tests in watch mode
21
- npx jest --coverage # Run tests with coverage report
22
- ```
23
-
24
- ### Running Single Tests
25
- ```bash
26
- npx jest yaft.spec.ts # Run specific test file
27
- npx jest -t "test name" # Run specific test by name pattern
28
- ```
29
-
30
- ## Architecture
31
-
32
- ### Core Components
33
-
34
- **FeatureToggle Decorator (`src/FeatureToggle.ts`)**
35
- - Main decorator function that can be applied to classes or methods
36
- - Evaluates feature state at decoration time using the configured provider
37
- - Returns fallback implementations when features are disabled
38
-
39
- **FeatureToggleBase Class**
40
- - Static container for the global `featureProvider` instance
41
- - Must be configured before using any `@FeatureToggle` decorators
42
-
43
- **FeatureProvider Interface**
44
- - Defines contract for data sources: `isEnabled(key: string): boolean`
45
- - Optional API integration with `apiUrl`, `baseUUID`, and collection hash support
46
- - Flexible data structure support via generic type parameter
47
-
48
- ### Provider Implementations
49
-
50
- **API Providers** (`src/examples/ApiService*Provider.ts`)
51
- - Integrate with YaFT Go backend via HTTP API
52
- - Support collection hash for efficient cache invalidation
53
- - Handle both boolean and Feature data types
54
- - Include error handling for network failures
55
-
56
- **Local Storage Providers** (`src/examples/LocalStorage*Provider.ts`)
57
- - Load configuration from local JSON files using `require()`
58
- - Support both boolean and Feature data types
59
- - Fallback to empty configuration on file load errors
60
-
61
- **Evaluation Core (`src/evaluate.ts`)**
62
- - `evaluate(feature, now)` is the single definition of the evaluation rules
63
- - Providers call it instead of implementing the logic themselves; before this
64
- existed, the feature providers each carried their own copy
65
- - `Clock` is a `() => number`; providers take one and default to `systemClock`,
66
- so tests and the conformance adapter can pin `now`
67
- - `parseTimestamp` accepts only RFC 3339 with an offset and returns
68
- `undefined` for anything else, warning rather than throwing
69
-
70
- ### Feature Data Model
71
-
72
- ```typescript
73
- type Feature = {
74
- key: string; // Unique feature identifier
75
- value: string; // Boolean value as string; only "true" is on
76
- activeAt: string; // RFC 3339 with offset, or empty
77
- disabledAt: string; // RFC 3339 with offset, or empty
78
- tags?: string[];
79
- }
80
- ```
81
-
82
- Features support time-based activation/deactivation logic evaluated at runtime.
83
- The window is half-open: `now == activeAt` is on, `now == disabledAt` is off.
84
- Dates without an offset are ignored, because languages disagree on how to read
85
- them and a feature would otherwise flip at a different instant per port.
86
-
87
- ### Decorator Behavior
88
-
89
- **Method Decoration:**
90
- - Replaces method implementation based on feature state
91
- - Supports fallback method when feature is disabled
92
- - Returns empty function if no fallback provided
93
-
94
- **Class Decoration:**
95
- - Replaces entire class constructor and prototype
96
- - Supports fallback class when feature is disabled
97
- - Creates EmptyClass with stub methods if no fallback provided
98
- - Preserves async method signatures in EmptyClass stubs
99
-
100
- ## Usage Patterns
101
-
102
- ### Initialization
103
- ```typescript
104
- import { FeatureToggleBase } from "@tehw0lf/yaft";
105
- import { LocalStorageFeatureProvider } from "./examples/LocalStorageFeatureProvider";
106
-
107
- FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider("./config.json");
108
- ```
109
-
110
- ### Method Toggle
111
- ```typescript
112
- @FeatureToggle("myFeature", fallbackMethod)
113
- myMethod() {
114
- // Implementation when feature is enabled
115
- }
116
- ```
117
-
118
- ### Class Toggle
119
- ```typescript
120
- @FeatureToggle("myFeature", FallbackClass)
121
- class MyClass {
122
- // Implementation when feature is enabled
123
- }
124
- ```
125
-
126
- ## Testing Strategy
127
-
128
- Tests use LocalStorageFeatureProvider with test configuration files (`src/test/test-*.json`). The test suite demonstrates:
129
- - Class decoration with fallback classes
130
- - Method decoration with and without fallbacks
131
- - Async method handling
132
- - Provider configuration and feature evaluation
133
-
134
- ## Build Configuration
135
-
136
- - **TypeScript**: CommonJS output targeting ES2022
137
- - **Decorators**: Experimental decorators enabled for `@FeatureToggle` syntax
138
- - **Output**: Compiled to `dist/yaft/` with declaration files
139
- - **Package**: Strips devDependencies during build process
140
-
141
- ## Dependencies
142
-
143
- - **Runtime**: `reflect-metadata` for decorator metadata
144
- - **Development**: TypeScript, Jest, ts-jest for testing
145
- - **Optional**: `axios` for API provider implementations
146
-
147
- ## Pre-commit Validation
148
-
149
- ```bash
150
- npm test && npm run build
151
- ```
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Robert Weyres
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/conformance.lock DELETED
@@ -1,2 +0,0 @@
1
- version=v1.1.0
2
- sha256=d83ff1c960ad29830c00b57727591da628f4323b777faea245f603565d1c9ae9
package/jest.config.js DELETED
@@ -1,16 +0,0 @@
1
- /** @type {import('ts-jest').JestConfigWithTsJest} **/
2
- module.exports = {
3
- displayName: 'yaft',
4
- setupFilesAfterEnv: [
5
- '<rootDir>/src/test/test-setup.ts'
6
- ],
7
- globals: {},
8
- coverageDirectory: 'coverage/yaft',
9
- testEnvironment: "node",
10
- testMatch: [
11
- '<rootDir>/src/**/*.spec.ts'
12
- ],
13
- transform: {
14
- "^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.spec.json" }],
15
- },
16
- };
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Fetches the YaFT conformance suite pinned in conformance.lock.
4
- #
5
- # Copy this into a port, next to a conformance.lock of the form:
6
- #
7
- # version=v1.0.0
8
- # sha256=<checksum of cases.tar.gz>
9
- #
10
- # The checksum is not optional. A Git tag can be moved; verifying the asset is
11
- # what stops a port's tests from changing without a diff.
12
- #
13
- # Usage: scripts/fetch-conformance.sh [target-dir] (default: src/test/conformance)
14
- #
15
- # Set the default to wherever the port's adapter reads the cases from. Ports
16
- # differ -- yaft-ts uses src/test/conformance -- and a default that does not
17
- # match leaves the next test run reporting missing cases.
18
-
19
- set -euo pipefail
20
-
21
- REPO="tehw0lf/yaft-conformance"
22
- LOCK="${LOCK:-conformance.lock}"
23
- TARGET="${1:-src/test/conformance}"
24
-
25
- if [[ ! -f "$LOCK" ]]; then
26
- echo "error: $LOCK not found; run from the port's root" >&2
27
- exit 1
28
- fi
29
-
30
- version="$(grep -E '^version=' "$LOCK" | cut -d= -f2-)"
31
- expected="$(grep -E '^sha256=' "$LOCK" | cut -d= -f2-)"
32
-
33
- if [[ -z "$version" || -z "$expected" ]]; then
34
- echo "error: $LOCK needs both version= and sha256=" >&2
35
- exit 1
36
- fi
37
-
38
- tmp="$(mktemp -d)"
39
- trap 'rm -rf "$tmp"' EXIT
40
-
41
- url="https://github.com/${REPO}/releases/download/${version}/cases.tar.gz"
42
- echo "fetching conformance suite ${version}"
43
- curl --fail --location --silent --show-error --output "$tmp/cases.tar.gz" "$url"
44
-
45
- # sha256sum is GNU coreutils and is not present on macOS; shasum ships with
46
- # both. Preferring sha256sum keeps Linux CI on the faster binary.
47
- if command -v sha256sum >/dev/null 2>&1; then
48
- actual="$(sha256sum "$tmp/cases.tar.gz" | cut -d' ' -f1)"
49
- elif command -v shasum >/dev/null 2>&1; then
50
- actual="$(shasum -a 256 "$tmp/cases.tar.gz" | cut -d' ' -f1)"
51
- else
52
- echo "error: neither sha256sum nor shasum found; cannot verify the download" >&2
53
- exit 1
54
- fi
55
- if [[ "$actual" != "$expected" ]]; then
56
- echo "error: checksum mismatch for cases.tar.gz" >&2
57
- echo " expected $expected" >&2
58
- echo " actual $actual" >&2
59
- echo "The tag may have been moved. Do not update the lock without reading the diff." >&2
60
- exit 1
61
- fi
62
-
63
- rm -rf "$TARGET"
64
- mkdir -p "$TARGET"
65
- tar --extract --gzip --file "$tmp/cases.tar.gz" --directory "$TARGET"
66
-
67
- echo "conformance suite ${version} unpacked into ${TARGET}"
@@ -1,95 +0,0 @@
1
- import "reflect-metadata";
2
-
3
- class EmptyClass {
4
- constructor() {}
5
- }
6
-
7
- export type Feature = {
8
- key: string;
9
- value: string;
10
- activeAt: string;
11
- disabledAt: string;
12
- tags?: string[];
13
- };
14
-
15
- export interface FeatureProvider<T> {
16
- apiUrl?: string;
17
- baseUUID?: string;
18
- data: Record<string, T>;
19
- getCollectionHash?(configPathOrUrl: string): void;
20
- getConfig(configPathOrUrl: string): void;
21
- isEnabled(key: string): boolean;
22
- }
23
-
24
- export function FeatureToggle(key: string, fallback?: any) {
25
- if (!FeatureToggleBase.featureProvider) {
26
- throw new Error("FeatureToggleProvider not set");
27
- }
28
- return (
29
- target: any,
30
- propertyKey?: string,
31
- descriptor?: PropertyDescriptor
32
- ) => {
33
- if (propertyKey && descriptor) {
34
- // Method
35
- const originalMethod = descriptor.value;
36
-
37
- // An async method must keep returning a promise when it is switched
38
- // off, or `await` at the call site breaks on a plain undefined. The
39
- // empty class shell below already makes this distinction; without it
40
- // here, turning a feature off would throw inside unrelated code.
41
- const isAsync =
42
- originalMethod?.[Symbol.toStringTag] === "AsyncFunction";
43
-
44
- descriptor.value = function (...args: any[]) {
45
- const isEnabled = FeatureToggleBase.featureProvider.isEnabled(key);
46
- if (isEnabled) {
47
- return originalMethod.apply(this, args);
48
- } else {
49
- if (fallback !== undefined) {
50
- const result = fallback.apply(this, args as []);
51
- // A synchronous fallback on an async method would otherwise hand
52
- // back a plain value, breaking the promise the signature
53
- // advertises. Promise.resolve passes an existing promise through
54
- // unchanged, so an async fallback is unaffected.
55
- return isAsync ? Promise.resolve(result) : result;
56
- }
57
- return isAsync ? Promise.resolve() : undefined;
58
- }
59
- };
60
- return descriptor;
61
- } else {
62
- // Class
63
- const originalConstructor = target;
64
- let newConstructor: any;
65
-
66
- const isEnabled = FeatureToggleBase.featureProvider.isEnabled(key);
67
- if (isEnabled) {
68
- newConstructor = originalConstructor;
69
- } else {
70
- newConstructor = fallback !== undefined ? fallback : EmptyClass;
71
- if (fallback) newConstructor.__proto__ = fallback.__proto__;
72
-
73
- if (newConstructor === EmptyClass) {
74
- Object.getOwnPropertyNames(originalConstructor.prototype).forEach(
75
- (name: string) => {
76
- if (name === "constructor") return;
77
- if (typeof originalConstructor.prototype[name] === "function")
78
- newConstructor.prototype[name] = () => {};
79
- if (
80
- originalConstructor.prototype[name][Symbol.toStringTag] ===
81
- "AsyncFunction"
82
- )
83
- newConstructor.prototype[name] = async () => {};
84
- }
85
- );
86
- }
87
- }
88
- return newConstructor;
89
- }
90
- };
91
- }
92
-
93
- export abstract class FeatureToggleBase {
94
- static featureProvider: FeatureProvider<any>;
95
- }