@tehw0lf/yaft
v0.0.22
Published
YaFT - Feature Toggles using Go&PostgreSQL or any source!
Downloads
1,645
Maintainers
Readme
YaFT for TypeScript
This provides a client for YaFT which aims to bring simple feature toggles for Methods and Classes to Typescript.
Installation
npm install @tehw0lf/yaftThe example API providers use axios and declare it as an optional peer dependency, so install it too if you use them:
npm install @tehw0lf/yaft axiosInitialization
To be able to use YaFT, implement and set a FeatureProvider on the abstract Base Class, or copy and adapt one of the example providers.
In this example a local json file with the Feature data type is used, but in theory any data type can be used since the Provider implements the isEnabled function which can be overridden as necessary.
import { FeatureToggleBase } from "@tehw0lf/yaft";
import { LocalStorageFeatureProvider } from "./provider";
FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider(
"./test-feature.json"
);This repo contains examples for two provider types: API Providers for boolean and Feature and LocalStorage Providers for boolean and Feature. The APi providers are designed to work with the default Go implementation of YaFT, but can be implemented through their interface:
export interface FeatureProvider<T> {
apiUrl?: string;
baseUUID?: string;
data: Record<string, T>;
initConfig(configPathOrUrl: string): void;
isEnabled(key: string): boolean;
}Usage
To manage anything with a feature toggle, decorate it with the feature's key:
import { FeatureToggle } from "@tehw0lf/yaft";
@FeatureToggle("myKey")
class MyClass {}import { FeatureToggle } from "@tehw0lf/yaft";
@FeatureToggle("myKey")
myMethod() {}If the feature is enabled, the original class/method will be used.
If the feature is disabled, a fallback can be provided:
import { FeatureToggle } from "@tehw0lf/yaft";
@FeatureToggle("myKey", MyFallbackClass)
class MyClass {}import { FeatureToggle } from "@tehw0lf/yaft";
@FeatureToggle("myKey", myFallbackMethod)
myMethod() {}Otherwise, or if the feature does not exist, the class/method gets replaced with an empty object/an empty function.
Collection Hash
The default YaFT API provides a collection hash to efficiently check whether the locally cached features are still up to date. See API provider examples for details. This can be scheduled to automatically update feature data in the background.
Since 0.0.21 the API providers have two ways to refresh:
refresh()reports the outcome. It resolves totruewhen new data was loaded andfalsewhen the group was unchanged, and rejects when the backend cannot be reached or answers with something that is not a hash or not a toggle group. The previous data stays in place either way.getCollectionHash(url)is the quiet variant for timers: it logs a failure instead of rejecting.
Refreshes run one after another. The constructor already starts one, so a
refresh() right after it waits for that one and then usually resolves to
false: the group is loaded and unchanged. If the constructor's refresh
failed, this one tries again and rejects if it fails too.
const provider = new ApiServiceFeatureProvider(apiUrl, groupUuid);
// At startup, or wherever a failure should be seen:
await provider.refresh();
// In the background:
setInterval(() => provider.getCollectionHash(`${apiUrl}/collectionHash/${groupUuid}`), 60_000);A /collectionHash answer without a hash now fails the refresh. Before, it was
recorded as undefined, which matched every later answer without one, so the
provider stopped refreshing without saying so.
Feature Data Type
The default data type for YaFT is Feature. A feature has a key string, a value string representing a boolean and optional activeAt and disabledAt date strings.
export type Feature = {
key: string;
value: string;
activeAt: string;
disabledAt: string;
tags?: string[];
};Changed in 0.0.22. A response whose key or value is not a string, such
as "value": true, no longer has it converted with String(): the field is
not set, so the feature is off and an entry without a string key is skipped.
JSON booleans belong in the boolean providers.
Evaluation rules
These rules are not this library's own: they are yaft-conformance, the shared specification every YaFT implementation is checked against. This port passes the whole suite (see Conformance below), so a feature evaluates identically here and in any other port.
A feature is on when all of the following hold. evaluate is exported, so the
rules can be applied directly to a feature without going through a provider.
- The value is exactly
"true"."TRUE","1"and""are off -- the value is stored as a string, and anything else would be a silent disagreement between backend and client. activeAthas passed, if set. The bound is inclusive: at exactlyactiveAtthe feature is on.disabledAthas not been reached, if set. This bound is exclusive: at exactlydisabledAtthe feature is off.
A missing feature is off. Unset, null or unparseable dates are ignored rather
than treated as an error, and never throw.
API providers and a failing backend
ApiServiceFeatureProvider and ApiServiceBooleanProvider keep their data
when a refresh fails — and, since 0.0.18, also when /features answers
200 with something that is not a toggle group (null, an array, a proxy's
error page). Before, that replaced the data with nothing and switched every
feature off. A failed fetch is now also retried on the next
getCollectionHash; before, one failure stopped refreshing until the backend
changed again. normaliseGroup is exported for providers of your own.
Boolean shape
The boolean providers hold { "myToggle": true } and have no time logic. Only
the JSON boolean true is on. A JSON false is kept in the data and reads as
off. Any value that is not a boolean -- "true", "false", 1, null -- is
dropped when the data loads, so its key is missing from the data and reads as
off. normaliseBooleans is exported and applies the same rule.
Changed in 0.0.17. Before, isEnabled returned whatever was stored, and a
caller testing it with if turned the string "false" on, because a
non-empty string is truthy. A configuration that relied on string or number
values has to switch to real booleans.
Date format
Dates must be RFC 3339 with an offset (2026-09-18T15:00:00Z or
2026-09-18T15:00:00+02:00). Anything else -- a bare date such as
2026-09-18, or a timestamp without an offset -- is ignored and logged as a
warning.
This is stricter than Date.parse, on purpose: JavaScript reads a bare date as
UTC midnight and an offset-less timestamp as local time, while most other
languages read both as local. Accepting them would make a feature flip at a
different instant depending on which client evaluated it.
Testing with a fixed time
Both Feature providers take an optional clock, so a test can evaluate against
a fixed instant instead of the current time:
import { LocalStorageFeatureProvider } from "./provider";
const provider = new LocalStorageFeatureProvider(
"./test-feature.json",
() => Date.parse("2026-09-18T12:00:00Z")
);The clock defaults to the system time, so existing code needs no change.
Conformance
The rules above are specified once, language-neutrally, in yaft-conformance, and this port is tested against that suite rather than only against its own expectations.
The version is pinned in conformance.lock:
version=v4.0.0
sha256=8e9dd45e897d0527b28f29a31d35736e20b4429b7280d21e5755aeaaaf4bf3c7npm test fetches that release, verifies the checksum and unpacks it before
Jest runs, so there is no separate step to forget and no way to get a green run
against stale cases. Upgrading the suite is a one-line change to that file,
visible in review.
The checksum is not decoration: a Git tag can be moved, and without verifying the asset a port's tests could change with no diff at all.
The adapter lives in src/test/conformance-adapter/. It fails loudly on a case
whose target, toggle or expected it does not implement, rather than
skipping it -- a silently skipped case is a rule that nothing enforces.
Licenses
- Code: MIT License
- Logo/Branding: All rights reserved
