Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/db/tenant-cache.server.ts
T

87 lines
3.1 KiB
TypeScript

import { createTwoTierCache, type TwoTierCache } from "@toolctrl/shared-store";
import { getSharedStore } from "~/shared-store.server";
import { clearCacheKey, clearCacheKeyPrefix } from "~/utils/cache.server";
import { createScopedLogger } from "~/utils/logger.server";
/**
* Tenant cache (two-tier). The existing in-process `cachified` LRU in
* `tenants.db.server.ts` stays the L1 read path; this module owns the
* pub/sub channel so peer replicas drop their L1 entries when a
* mutation happens on a different replica.
*
* Keys broadcast over the channel are tenant ids. The subscriber clears
* every cachified key derived from a tenant (`tenant:`, `tenantSimple:`,
* `tenantIdOrSlug:`).
*/
const log = createScopedLogger("tenant-cache");
const TTL_SECONDS_DEFAULT = 5 * 60;
let cacheSingleton: TwoTierCache<{ id: string } | null> | null = null;
let subscribeDisposer: (() => Promise<void>) | null = null;
function pubsubEnabledFromEnv(): boolean {
const raw = process.env.CACHE_INVALIDATION_ENABLED;
if (raw === undefined || raw === null || raw === "") return true;
return raw.toLowerCase() !== "false" && raw !== "0";
}
function ttlSecondsFromEnv(): number {
const raw = process.env.CACHE_L1_TTL_SECONDS;
if (!raw) return TTL_SECONDS_DEFAULT;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return TTL_SECONDS_DEFAULT;
return Math.floor(parsed);
}
export function getTenantCache(): TwoTierCache<{ id: string } | null> {
if (cacheSingleton) return cacheSingleton;
cacheSingleton = createTwoTierCache<{ id: string } | null>({
store: getSharedStore(),
namespace: "tenant",
ttlSeconds: ttlSecondsFromEnv(),
pubsubEnabled: pubsubEnabledFromEnv(),
logger: { warn: (obj, msg) => log.warn(obj, msg), error: (obj, msg) => log.error(obj, msg) },
});
return cacheSingleton;
}
/**
* Publish a tenant-invalidation. Call after any tenant mutation in the
* web app (settings update, logo upload, member add/remove). The local
* cachified entries are also cleared as a defence-in-depth in case the
* caller forgot to call clearCacheKey().
*/
export async function publishTenantInvalidation(tenantId: string): Promise<void> {
clearCacheKey(`tenant:${tenantId}`);
clearCacheKey(`tenantSimple:${tenantId}`);
clearCacheKey(`tenantIdOrSlug:${tenantId}`);
await getTenantCache().invalidate(tenantId).catch(() => {
/* logged inside helper */
});
}
export async function subscribeTenantCache(): Promise<() => Promise<void>> {
if (subscribeDisposer) return subscribeDisposer;
const cache = getTenantCache();
subscribeDisposer = await cache.subscribe((key) => {
if (!key || key === "*") {
clearCacheKeyPrefix("tenant:");
clearCacheKeyPrefix("tenantSimple:");
clearCacheKeyPrefix("tenantIdOrSlug:");
return;
}
clearCacheKey(`tenant:${key}`);
clearCacheKey(`tenantSimple:${key}`);
clearCacheKey(`tenantIdOrSlug:${key}`);
});
log.info({ namespace: "tenant" }, "tenant cache subscriber started");
return subscribeDisposer;
}
/** Test seam: drop the singleton + disposer. */
export function __resetTenantCacheForTests(): void {
cacheSingleton = null;
subscribeDisposer = null;
}