feat(image-pipeline): add backend rollout flags

This commit is contained in:
Matthias
2026-04-04 21:33:00 +02:00
parent 8fb5482550
commit fd4f8f4f3b
3 changed files with 416 additions and 4 deletions

View File

@@ -9,6 +9,10 @@ import {
type ImagePipelineBackend,
type PreviewBackendRequest,
} from "@/lib/image-pipeline/backend/backend-types";
import {
getBackendFeatureFlags,
type BackendFeatureFlags,
} from "@/lib/image-pipeline/backend/feature-flags";
type BackendFallbackReason = "unsupported_api" | "flag_disabled" | "runtime_error";
@@ -59,15 +63,50 @@ export function createBackendRouter(options?: {
backends?: readonly ImagePipelineBackend[];
defaultBackendId?: string;
backendAvailability?: Readonly<Record<string, BackendAvailability>>;
featureFlags?: BackendFeatureFlags;
onFallback?: (event: BackendFallbackEvent) => void;
}): BackendRouter {
const configuredBackends = options?.backends?.length ? [...options.backends] : [cpuBackend];
const byId = new Map(configuredBackends.map((backend) => [backend.id.toLowerCase(), backend]));
const defaultBackend =
const configuredDefaultBackend =
byId.get(options?.defaultBackendId?.toLowerCase() ?? "") ??
byId.get(CPU_BACKEND_ID) ??
configuredBackends[0] ??
cpuBackend;
const cpuFallbackBackend = byId.get(CPU_BACKEND_ID) ?? configuredDefaultBackend;
const featureFlags = options?.featureFlags;
function isBackendEnabledByFlags(backendId: string): boolean {
if (!featureFlags) {
return true;
}
const normalizedBackendId = backendId.toLowerCase();
if (featureFlags.forceCpu) {
return normalizedBackendId === CPU_BACKEND_ID;
}
if (normalizedBackendId === "webgl") {
return featureFlags.webglEnabled;
}
if (normalizedBackendId === "wasm") {
return featureFlags.wasmEnabled;
}
return true;
}
function resolveDefaultBackend(): ImagePipelineBackend {
if (!isBackendEnabledByFlags(configuredDefaultBackend.id)) {
return cpuFallbackBackend;
}
return configuredDefaultBackend;
}
const defaultBackend = resolveDefaultBackend();
const normalizedDefaultId = defaultBackend.id.toLowerCase();
function readAvailability(backendId: string): BackendAvailability | undefined {
@@ -102,6 +141,14 @@ export function createBackendRouter(options?: {
}
const availability = readAvailability(normalizedHint);
if (!isBackendEnabledByFlags(normalizedHint)) {
return {
backend: defaultBackend,
fallbackReason: "flag_disabled",
requestedBackend: normalizedHint,
};
}
if (availability?.enabled === false) {
return {
backend: defaultBackend,
@@ -190,12 +237,14 @@ export function createBackendRouter(options?: {
};
}
const defaultRouter = createBackendRouter();
const rolloutRouter = createBackendRouter({
featureFlags: getBackendFeatureFlags(),
});
export function runPreviewStepWithBackendRouter(request: PreviewBackendRequest): void {
defaultRouter.runPreviewStep(request);
rolloutRouter.runPreviewStep(request);
}
export function runFullPipelineWithBackendRouter(request: FullBackendRequest): void {
defaultRouter.runFullPipeline(request);
rolloutRouter.runFullPipeline(request);
}

View File

@@ -0,0 +1,100 @@
export const IMAGE_PIPELINE_BACKEND_FLAG_KEYS = {
forceCpu: "imagePipeline.backend.forceCpu",
webglEnabled: "imagePipeline.backend.webgl.enabled",
wasmEnabled: "imagePipeline.backend.wasm.enabled",
} as const;
export type BackendFeatureFlags = {
forceCpu: boolean;
webglEnabled: boolean;
wasmEnabled: boolean;
};
export type BackendFeatureFlagReader = (
key: (typeof IMAGE_PIPELINE_BACKEND_FLAG_KEYS)[keyof typeof IMAGE_PIPELINE_BACKEND_FLAG_KEYS],
) => unknown;
const DEFAULT_BACKEND_FEATURE_FLAGS: BackendFeatureFlags = {
forceCpu: false,
webglEnabled: false,
wasmEnabled: false,
};
type RuntimeFeatureFlagStore = {
[IMAGE_PIPELINE_BACKEND_FLAG_KEYS.forceCpu]?: unknown;
[IMAGE_PIPELINE_BACKEND_FLAG_KEYS.webglEnabled]?: unknown;
[IMAGE_PIPELINE_BACKEND_FLAG_KEYS.wasmEnabled]?: unknown;
};
declare global {
interface Window {
__LEMONSPACE_FEATURE_FLAGS__?: RuntimeFeatureFlagStore;
}
var __LEMONSPACE_FEATURE_FLAGS__: RuntimeFeatureFlagStore | undefined;
}
function parseBooleanFlag(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "number") {
if (value === 1) {
return true;
}
if (value === 0) {
return false;
}
return undefined;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1" || normalized === "on") {
return true;
}
if (normalized === "false" || normalized === "0" || normalized === "off") {
return false;
}
}
return undefined;
}
function readFlagFromRuntimeStore(
key: (typeof IMAGE_PIPELINE_BACKEND_FLAG_KEYS)[keyof typeof IMAGE_PIPELINE_BACKEND_FLAG_KEYS],
): unknown {
const runtimeStore =
globalThis.__LEMONSPACE_FEATURE_FLAGS__ ??
(typeof window !== "undefined" ? window.__LEMONSPACE_FEATURE_FLAGS__ : undefined);
if (runtimeStore && key in runtimeStore) {
return runtimeStore[key];
}
try {
if (typeof localStorage !== "undefined") {
return localStorage.getItem(key);
}
} catch {
return undefined;
}
return undefined;
}
export function getBackendFeatureFlags(readFlag?: BackendFeatureFlagReader): BackendFeatureFlags {
const reader = readFlag ?? readFlagFromRuntimeStore;
return {
forceCpu:
parseBooleanFlag(reader(IMAGE_PIPELINE_BACKEND_FLAG_KEYS.forceCpu)) ??
DEFAULT_BACKEND_FEATURE_FLAGS.forceCpu,
webglEnabled:
parseBooleanFlag(reader(IMAGE_PIPELINE_BACKEND_FLAG_KEYS.webglEnabled)) ??
DEFAULT_BACKEND_FEATURE_FLAGS.webglEnabled,
wasmEnabled:
parseBooleanFlag(reader(IMAGE_PIPELINE_BACKEND_FLAG_KEYS.wasmEnabled)) ??
DEFAULT_BACKEND_FEATURE_FLAGS.wasmEnabled,
};
}