import { createScopedLogger } from "./utils/logger.server"; import { subscribeSubscriptionCache } from "./utils/services/.server/subscriptionCache.server"; import { subscribeTenantCache } from "./utils/db/tenant-cache.server"; import { subscribeFeatureFlagCache } from "./utils/services/.server/featureFlagCache.server"; import { wireTenantPlanLookup } from "./utils/services/.server/tenantPlanLookup.server"; import { getSharedStore } from "./shared-store.server"; import { subscribePermissionCache } from "./utils/db/permissions/permission-cache.server"; import { subscribeApiKeyAuthCache } from "./utils/api/api-key-auth-cache.server"; import { subscribeUserSnapshotCache } from "./utils/db/user-snapshot-cache.server"; import { subscribeAppConfigurationCache } from "./utils/db/app-configuration-cache.server"; const log = createScopedLogger("cache-bootstrap"); let started = false; function pubsubEnabled(): boolean { const raw = process.env.CACHE_INVALIDATION_ENABLED; if (raw === undefined || raw === null || raw === "") return true; return raw.toLowerCase() !== "false" && raw !== "0"; } /** * Idempotent boot hook. Starts every cache-invalidation subscriber this * web replica needs to keep its in-process L1 caches consistent with the * SharedStore L2 (and with mutations originating in other apps such as * the billing webhook in apps/api). * * Safe to call multiple times — only the first invocation wires the * subscribers; subsequent calls are no-ops. Failures are logged but * intentionally swallowed: the L1 cache TTL is the safety net. */ export function startCacheSubscribers(): void { if (started) return; // Resolve the backing store synchronously at boot. Production must never // continue with replica-local cache and rate-limit state when Redis is // missing; asynchronous subscriber errors would otherwise be swallowed. getSharedStore(); started = true; // Wire the BullMQ tenant-plan lookup unconditionally — it reads from the // subscription cache which still serves locally when pub/sub is off. wireTenantPlanLookup(); if (!pubsubEnabled()) { log.warn( { component: "cache-bootstrap" }, "CACHE_INVALIDATION_ENABLED=false — pub/sub subscribers will not start (L1 TTL is the only consistency mechanism)" ); return; } void Promise.all([ subscribeSubscriptionCache().catch((err: unknown) => { log.error({ err }, "subscription cache subscriber failed to start"); }), subscribeTenantCache().catch((err: unknown) => { log.error({ err }, "tenant cache subscriber failed to start"); }), subscribeFeatureFlagCache().catch((err: unknown) => { log.error({ err }, "feature-flag cache subscriber failed to start"); }), subscribePermissionCache().catch((err: unknown) => { log.error({ err }, "permission cache subscriber failed to start"); }), subscribeApiKeyAuthCache().catch((err: unknown) => { log.error({ err }, "API-key authorization cache subscriber failed to start"); }), subscribeUserSnapshotCache().catch((err: unknown) => { log.error({ err }, "user snapshot cache subscriber failed to start"); }), subscribeAppConfigurationCache().catch((err: unknown) => { log.error({ err }, "app configuration cache subscriber failed to start"); }), ]).then(() => { log.info({ component: "cache-bootstrap" }, "cache subscribers started"); }); } /** Test seam: reset the started flag. */ export function __resetCacheBootstrapForTests(): void { started = false; }