85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
/**
|
|
* isSplitEnabled() is the Wave-0 gate. The entire migration/routing/FK-drop family
|
|
* MUST be unreachable when this returns false. Default is false (single-DB). Never
|
|
* infer split-vs-single from URL string-equality — distinctness is proven by the
|
|
* runtime sentinel.
|
|
*/
|
|
import { env } from "~/env.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { probeDistinctDatabases as defaultProbe } from "./distinctDbSentinel.server";
|
|
|
|
export type SplitModeConfig = {
|
|
flagEnabled: boolean;
|
|
legacyUrl?: string;
|
|
newUrl?: string;
|
|
};
|
|
|
|
export type SplitModeDeps = {
|
|
probe?: typeof defaultProbe;
|
|
logger?: { warn: (msg: string, meta?: Record<string, unknown>) => void };
|
|
};
|
|
|
|
export async function computeSplitEnabled(
|
|
config: SplitModeConfig,
|
|
deps: SplitModeDeps = {}
|
|
): Promise<boolean> {
|
|
// Hard gate #1: explicit positive opt-in. OFF by default -> never probe.
|
|
if (!config.flagEnabled) {
|
|
return false;
|
|
}
|
|
// Both URLs are required to even consider a split.
|
|
if (!config.legacyUrl || !config.newUrl) {
|
|
deps.logger?.warn(
|
|
"RUN_OPS_SPLIT_ENABLED is on but RUN_OPS_LEGACY_DATABASE_URL / RUN_OPS_DATABASE_URL are not both set; staying single-DB."
|
|
);
|
|
return false;
|
|
}
|
|
// Hard gate #2: runtime sentinel must confirm physically-distinct DBs.
|
|
const probe = deps.probe ?? defaultProbe;
|
|
const result = await probe(config.legacyUrl, config.newUrl, { logger: deps.logger });
|
|
return result.distinct === true;
|
|
}
|
|
|
|
export type SplitRealtimeInterlockConfig = {
|
|
splitEnabled: boolean;
|
|
nativeRealtimeEnabled: boolean;
|
|
};
|
|
|
|
/**
|
|
* Boot-time realtime interlock (pure predicate). Split mode puts NEW-resident
|
|
* (run-ops id) runs on the dedicated run-ops DB, but Electric replicates only from the
|
|
* control-plane DB — with the native realtime backend OFF those runs are invisible
|
|
* and every realtime subscription hangs. Refuse split unless native is on; split-off
|
|
* is always allowed regardless of the realtime backend.
|
|
*/
|
|
export function assertSplitRealtimeInterlock(config: SplitRealtimeInterlockConfig): void {
|
|
if (!config.splitEnabled) {
|
|
return;
|
|
}
|
|
if (!config.nativeRealtimeEnabled) {
|
|
throw new Error(
|
|
"RUN_OPS_SPLIT_ENABLED is on but the native realtime backend (REALTIME_BACKEND_NATIVE_ENABLED) is not enabled — Electric cannot serve NEW-resident runs; refusing to enable split."
|
|
);
|
|
}
|
|
}
|
|
|
|
let cached: Promise<boolean> | undefined;
|
|
|
|
export function isSplitEnabled(): Promise<boolean> {
|
|
if (!cached) {
|
|
cached = computeSplitEnabled(
|
|
{
|
|
flagEnabled: env.RUN_OPS_SPLIT_ENABLED,
|
|
legacyUrl: env.RUN_OPS_LEGACY_DATABASE_URL,
|
|
newUrl: env.RUN_OPS_DATABASE_URL,
|
|
},
|
|
{ logger }
|
|
);
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
export function __resetSplitModeCacheForTests(): void {
|
|
cached = undefined;
|
|
}
|