Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import { createTwoTierCache, type TwoTierCache } from "@toolctrl/shared-store";
|
||||
import type { TenantSubscriptionWithDetails } from "@toolctrl/db/subscriptions.server";
|
||||
import { getSharedStore } from "~/shared-store.server";
|
||||
import { clearCacheKey, clearCacheKeyPrefix } from "~/utils/cache.server";
|
||||
import { createScopedLogger } from "~/utils/logger.server";
|
||||
|
||||
/**
|
||||
* Subscription cache (two-tier). L1 = in-process Map, L2 = SharedStore;
|
||||
* cross-replica purges via pub/sub on channel
|
||||
* `shared-store:web:ch:cache:subscription:invalidate`.
|
||||
*
|
||||
* Bootstrap (idempotent): call `subscribeSubscriptionCache()` once during
|
||||
* web boot so this replica listens for invalidation messages emitted by
|
||||
* the API stripe webhook handler.
|
||||
*/
|
||||
const log = createScopedLogger("subscription-cache");
|
||||
|
||||
const TTL_SECONDS_DEFAULT = 5 * 60; // 5 minutes — L1 safety-net so a missed pub/sub event self-heals quickly.
|
||||
|
||||
let cacheSingleton: TwoTierCache<TenantSubscriptionWithDetails | 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 getSubscriptionCache(): TwoTierCache<TenantSubscriptionWithDetails | null> {
|
||||
if (cacheSingleton) return cacheSingleton;
|
||||
cacheSingleton = createTwoTierCache<TenantSubscriptionWithDetails | null>({
|
||||
store: getSharedStore(),
|
||||
namespace: "subscription",
|
||||
ttlSeconds: ttlSecondsFromEnv(),
|
||||
pubsubEnabled: pubsubEnabledFromEnv(),
|
||||
logger: { warn: (obj, msg) => log.warn(obj, msg), error: (obj, msg) => log.error(obj, msg) },
|
||||
});
|
||||
return cacheSingleton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the subscriber for this process. Safe to call repeatedly (idempotent).
|
||||
* Returns the disposer for tests / graceful shutdown.
|
||||
*/
|
||||
export async function subscribeSubscriptionCache(): Promise<() => Promise<void>> {
|
||||
if (subscribeDisposer) return subscribeDisposer;
|
||||
const cache = getSubscriptionCache();
|
||||
// Bridge: on remote invalidation, also purge the in-process cachified
|
||||
// LRU keyed `tenantSubscription:<tenantId>` so getActiveTenantSubscriptions
|
||||
// refetches on the next call.
|
||||
subscribeDisposer = await cache.subscribe((key) => {
|
||||
if (!key || key === "*") {
|
||||
clearCacheKeyPrefix("tenantSubscription:");
|
||||
} else {
|
||||
clearCacheKey(`tenantSubscription:${key}`);
|
||||
}
|
||||
});
|
||||
log.info({ namespace: "subscription" }, "subscription cache subscriber started");
|
||||
return subscribeDisposer;
|
||||
}
|
||||
|
||||
/** Test seam: drop the singleton + disposer so each test gets a fresh cache. */
|
||||
export function __resetSubscriptionCacheForTests(): void {
|
||||
cacheSingleton = null;
|
||||
subscribeDisposer = null;
|
||||
}
|
||||
Reference in new issue
Block a user