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

110 lines
3.6 KiB
TypeScript

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";
import type { UserWithoutPassword } from "./users.db.server";
const log = createScopedLogger("user-snapshot-cache");
export const USER_SNAPSHOT_CACHE_TTL_SECONDS = 30;
type SerializedUserSnapshot = Omit<UserWithoutPassword, "createdAt"> & {
createdAt: string;
};
const userSerializer: CacheSerializer<UserWithoutPassword> = {
serialize(value) {
const serialized: SerializedUserSnapshot = {
...value,
createdAt: value.createdAt.toISOString(),
};
return JSON.stringify(serialized);
},
deserialize(raw) {
const value = JSON.parse(raw) as SerializedUserSnapshot;
return {
...value,
createdAt: new Date(value.createdAt),
};
},
};
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 createUserSnapshotCache(store: SharedStore, options: { now?: () => number } = {}): TwoTierCache<UserWithoutPassword> {
return createTwoTierCache<UserWithoutPassword>({
store,
namespace: "user-snapshot",
ttlSeconds: USER_SNAPSHOT_CACHE_TTL_SECONDS,
l1MaxEntries: 10_000,
l1Now: options.now,
serializer: userSerializer,
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<UserWithoutPassword> | null = null;
let subscribeDisposer: (() => Promise<void>) | null = null;
export function getUserSnapshotCache(): TwoTierCache<UserWithoutPassword> {
cacheSingleton ??= createUserSnapshotCache(getSharedStore());
return cacheSingleton;
}
export async function loadCachedUserSnapshot(userId: string, getFreshValue: () => Promise<UserWithoutPassword | null>): Promise<UserWithoutPassword | null> {
const cache = getUserSnapshotCache();
try {
const cached = await cache.get(userId);
if (cached !== null) return cached;
} catch (error) {
log.warn(
{ op: "user-snapshot-cache.read", err: { name: error instanceof Error ? error.name : "Error" } },
"User snapshot cache read failed; falling back to the database."
);
}
const fresh = await getFreshValue();
if (fresh !== null) {
try {
await cache.set(userId, fresh);
} catch (error) {
log.warn(
{ op: "user-snapshot-cache.write", err: { name: error instanceof Error ? error.name : "Error" } },
"User snapshot cache write failed; continuing with the database result."
);
}
}
return fresh;
}
/** Fail closed because this snapshot contains the operator-admin relation. */
export async function invalidateUserSnapshot(userId: string): Promise<void> {
clearCacheKey(`user:${userId}`);
const cache = getUserSnapshotCache();
cache.deleteLocal(userId);
await cache.l2.invalidate(userId);
}
export async function subscribeUserSnapshotCache(): Promise<() => Promise<void>> {
if (subscribeDisposer) return subscribeDisposer;
subscribeDisposer = await getUserSnapshotCache().subscribe();
log.info({ namespace: "user-snapshot" }, "User snapshot cache subscriber started.");
return subscribeDisposer;
}
/** Test seam. */
export function __resetUserSnapshotCacheForTests(): void {
cacheSingleton = null;
subscribeDisposer = null;
}