87 lines
3.1 KiB
TypeScript
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;
|
|
}
|