Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/db/app-configuration-cache.server.ts
T

102 lines
3.8 KiB
TypeScript

import type { AppConfiguration as AppConfigurationRow } from "@hivegrid/db/prisma-browser";
import { createTwoTierCache, type CacheSerializer, type SharedStore, type TwoTierCache } from "@hivegrid/shared-store";
import { getSharedStore } from "~/shared-store.server";
import { clearCacheKey } from "~/utils/cache.server";
import { createScopedLogger } from "~/utils/logger.server";
const log = createScopedLogger("app-configuration-cache");
export const APP_CONFIGURATION_CACHE_TTL_SECONDS = 30;
const APP_CONFIGURATION_CACHE_KEY = "global";
type SerializedAppConfiguration = Omit<AppConfigurationRow, "updatedAt"> & { updatedAt: string };
const appConfigurationSerializer: CacheSerializer<AppConfigurationRow> = {
serialize(value) {
return JSON.stringify({ ...value, updatedAt: value.updatedAt.toISOString() } satisfies SerializedAppConfiguration);
},
deserialize(raw) {
const value = JSON.parse(raw) as SerializedAppConfiguration;
return { ...value, updatedAt: new Date(value.updatedAt) };
},
};
function pubsubEnabledFromEnv(): boolean {
const raw = process.env.CACHE_INVALIDATION_ENABLED;
if (raw === undefined || raw === null || raw === "") return true;
return raw.toLowerCase() !== "false" && raw !== "0";
}
export function createAppConfigurationCache(store: SharedStore, options: { now?: () => number } = {}): TwoTierCache<AppConfigurationRow> {
return createTwoTierCache<AppConfigurationRow>({
store,
namespace: "app-configuration",
ttlSeconds: APP_CONFIGURATION_CACHE_TTL_SECONDS,
l1MaxEntries: 1,
l1Now: options.now,
serializer: appConfigurationSerializer,
pubsubEnabled: pubsubEnabledFromEnv(),
logger: {
warn: (obj, msg) => log.warn(obj, msg),
info: (obj, msg) => log.info(obj, msg),
error: (obj, msg) => log.error(obj, msg),
},
});
}
let cacheSingleton: TwoTierCache<AppConfigurationRow> | null = null;
let subscribeDisposer: (() => Promise<void>) | null = null;
function getAppConfigurationCache(): TwoTierCache<AppConfigurationRow> {
cacheSingleton ??= createAppConfigurationCache(getSharedStore());
return cacheSingleton;
}
export async function loadCachedAppConfiguration(getFreshValue: () => Promise<AppConfigurationRow | null>): Promise<AppConfigurationRow | null> {
const cache = getAppConfigurationCache();
try {
const cached = await cache.get(APP_CONFIGURATION_CACHE_KEY);
if (cached !== null) return cached;
} catch (error) {
log.warn(
{ op: "app-configuration-cache.read", err: { name: error instanceof Error ? error.name : "Error" } },
"App configuration cache read failed; falling back to the database."
);
}
const fresh = await getFreshValue();
if (fresh !== null) {
try {
await cache.set(APP_CONFIGURATION_CACHE_KEY, fresh);
} catch (error) {
log.warn(
{ op: "app-configuration-cache.write", err: { name: error instanceof Error ? error.name : "Error" } },
"App configuration cache write failed; continuing with the database result."
);
}
}
return fresh;
}
/** Fail closed because this row controls auth policy and injected scripts. */
export async function invalidateAppConfigurationCache(): Promise<void> {
clearCacheKey("appConfiguration");
const cache = getAppConfigurationCache();
cache.deleteLocal(APP_CONFIGURATION_CACHE_KEY);
await cache.l2.invalidate(APP_CONFIGURATION_CACHE_KEY);
}
export async function subscribeAppConfigurationCache(): Promise<() => Promise<void>> {
if (subscribeDisposer) return subscribeDisposer;
subscribeDisposer = await getAppConfigurationCache().subscribe();
log.info({ namespace: "app-configuration" }, "App configuration cache subscriber started.");
return subscribeDisposer;
}
/** Test seam. */
export function __resetAppConfigurationCacheForTests(): void {
cacheSingleton = null;
subscribeDisposer = null;
}