Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | 28x 28x 28x 11x 11x 11x 4x 5x 2x 2x 2x 13x 13x 4x 9x 9x 1x 1x 9x 9x 7x 2x | /**
* @module
*
* @author Bayu Dwiyan Satria
* @version 1.0.0
* @since 1.3.0
*/
import { Binding } from '@/core/bindings/Binding'
import { lazySettings } from '@/utils/lazySettings'
import { FlagshipSettings } from '@/types/FlagshipSettings'
import type { CloudflareEnv } from '@/types/CloudflareEnv'
/**
* Accessor for a Flagship feature-flag binding.
*
* @remarks
* Flagship evaluates a flag at request time, so a value that is a `vars` entry
* today — read once per deploy, changed only by deploying again — becomes
* something a targeting rule can move for a percentage of traffic, or for named
* callers first. That is the whole reason to reach for it: a rollback stops
* being a deploy.
*
* ## This one degrades instead of throwing, and that is the point
*
* Every other accessor here answers a missing binding with a
* `MissingBindingError`, because a Worker that lost its database cannot serve
* the request either way and should say so. A flag is the opposite. The value
* exists to decide between two paths that both work, so a Worker that cannot
* reach Flagship should take the path it would have taken before flags existed
* — not fail.
*
* So every method here takes a fallback and returns it when the binding is
* absent, when evaluation fails, or when the flag's type does not match. An
* evaluation error is logged and swallowed, the way {@link AnalyticsBinding}'s
* `writeSafe` treats a failed telemetry write: the caller asked a question that
* has a safe answer, and raising instead would make the flag service a new way
* for the Worker to be down.
*
* The corollary is that the fallback is not boilerplate. It is the deployed
* behaviour, and choosing it badly is how a flag outage becomes an incident —
* so pass the value the Worker used before the flag existed, which for the
* migration this package was written for means the `vars` entry it replaces.
*
* `resolve(env)` is still inherited and still throws, for the rare caller that
* genuinely cannot proceed without a live evaluation.
*
* ## Why there is no `FlagshipService`
*
* Same reason as {@link WorkflowBinding}: the kernel names no capability this
* could implement. Unlike durable execution, feature flagging is the strongest
* candidate in this package for one — it has many providers, and none of its
* vocabulary is Cloudflare's. When a second provider is actually in play, a
* `FeatureFlags` interface belongs in `@bayudwiyansatria/core` and this becomes
* its adapter. Declaring it now, against one implementation and no consumers,
* would be ceremony rather than abstraction.
*
* @example
* ```ts
* const flags = new FlagshipBinding()
*
* // env.RELEASE_CHANNEL is the deployed value and the fallback if Flagship is unreachable.
* const channel = await flags.string(env, 'release-channel', env.RELEASE_CHANNEL ?? 'stable')
*
* // Per-request attributes for a targeting rule.
* const enabled = await flags.boolean(env, 'new-checkout', false, { cohort })
* ```
*
* @class
*
* @author Bayu Dwiyan Satria
* @version 1.0.0
* @since 1.3.0
*/
export class FlagshipBinding<E extends CloudflareEnv = CloudflareEnv> extends Binding<Flagship, E> {
/**
* This accessor's settings, resolved on first read.
*/
private readonly settings: () => FlagshipSettings
/**
* Constructs a FlagshipBinding instance.
*
* @param settings Resolved settings. Defaults to `resolve('flagship')`, so callers
* normally construct this with no arguments at all.
*/
constructor(settings?: FlagshipSettings) {
const settle = lazySettings<FlagshipSettings>('flagship', settings)
super(() => settle().binding)
this.settings = settle
}
/**
* Evaluates a boolean flag.
*
* @param env The Worker environment.
* @param key The flag key, as it is named in the Flagship app.
* @param fallback The value to use when the flag cannot be evaluated. Pass the deployed behaviour.
* @param context Per-request attributes for targeting, merged over the configured context.
* @returns The evaluated value, or `fallback`.
*/
public async boolean(env: E, key: string, fallback: boolean, context?: FlagshipEvaluationContext): Promise<boolean> {
return await this.evaluate(env, fallback, flags => flags.getBooleanValue(key, fallback, this.context(context)))
}
/**
* Evaluates a string flag.
*
* @remarks
* Useful for named modes, where the deployed fallback is a string rather
* than a boolean.
*
* @param env The Worker environment.
* @param key The flag key, as it is named in the Flagship app.
* @param fallback The value to use when the flag cannot be evaluated. Pass the deployed behaviour.
* @param context Per-request attributes for targeting, merged over the configured context.
* @returns The evaluated value, or `fallback`.
*/
public async string(env: E, key: string, fallback: string, context?: FlagshipEvaluationContext): Promise<string> {
return await this.evaluate(env, fallback, flags => flags.getStringValue(key, fallback, this.context(context)))
}
/**
* Evaluates a numeric flag.
*
* @param env The Worker environment.
* @param key The flag key, as it is named in the Flagship app.
* @param fallback The value to use when the flag cannot be evaluated. Pass the deployed behaviour.
* @param context Per-request attributes for targeting, merged over the configured context.
* @returns The evaluated value, or `fallback`.
*/
public async number(env: E, key: string, fallback: number, context?: FlagshipEvaluationContext): Promise<number> {
return await this.evaluate(env, fallback, flags => flags.getNumberValue(key, fallback, this.context(context)))
}
/**
* Evaluates a structured flag.
*
* @typeParam T The shape the flag carries.
*
* @param env The Worker environment.
* @param key The flag key, as it is named in the Flagship app.
* @param fallback The value to use when the flag cannot be evaluated. Pass the deployed behaviour.
* @param context Per-request attributes for targeting, merged over the configured context.
* @returns The evaluated value, or `fallback`.
*/
public async object<T extends object>(
env: E,
key: string,
fallback: T,
context?: FlagshipEvaluationContext
): Promise<T> {
return await this.evaluate(env, fallback, flags => flags.getObjectValue<T>(key, fallback, this.context(context)))
}
/**
* Reports whether this deployment can evaluate flags at all.
*
* @remarks
* Rarely needed, since every method above already answers with its fallback.
* It is here for the one caller that wants to say so out loud — a health
* endpoint reporting degraded flag evaluation, or a log line explaining why
* every flag read the deployed value.
*
* @param env The Worker environment.
* @returns `true` when the binding is present.
*/
public isAvailable(env: E): boolean {
return this.isBound(env)
}
/**
* Runs an evaluation, answering with the fallback rather than raising.
*
* @typeParam T The flag's value type.
*
* @param env The Worker environment.
* @param fallback The value to answer with when evaluation cannot happen.
* @param evaluate The evaluation to attempt against the resolved binding.
* @returns The evaluated value, or `fallback`.
*/
private async evaluate<T>(env: E, fallback: T, evaluate: (flags: Flagship) => Promise<T>): Promise<T> {
const flags = this.tryResolve(env)
if (flags === null) {
return fallback
}
try {
return await evaluate(flags)
} catch (e) {
console.error('[Flagship Error]', e)
return fallback
}
}
/**
* Merges a call's evaluation context over the configured one.
*
* @remarks
* The call wins on any key both name. The configured context describes the
* deployment and the call describes the request, so the more specific of the
* two is the one that arrived last.
*
* @param context Per-request attributes, if the caller supplied any.
* @returns The combined context, or `undefined` when neither side has one.
*/
private context(context?: FlagshipEvaluationContext): FlagshipEvaluationContext | undefined {
const configured = this.settings().context
if (configured === undefined) {
return context
}
return context === undefined ? configured : { ...configured, ...context }
}
}
|