Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/.server/subscriptionCache.server.test.ts
T

100 lines
3.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MemoryAdapter, createSharedStore } from "@toolctrl/shared-store";
vi.mock("~/utils/cache.server", () => {
const map = new Map<string, unknown>();
return {
cache: map,
cachified: vi.fn(),
clearCacheKey: vi.fn((key: string) => map.delete(key)),
clearCacheKeyPrefix: vi.fn((prefix: string) => {
for (const k of map.keys()) if (k.startsWith(prefix)) map.delete(k);
}),
__test_map: map,
};
});
vi.mock("~/shared-store.server", async () => {
const { MemoryAdapter, createSharedStore } = await import("@toolctrl/shared-store");
const adapter = new MemoryAdapter();
const store = createSharedStore({ namespace: "web-test", adapter });
return {
getSharedStore: () => store,
__setSharedStoreFactoryForTests: vi.fn(),
__resetSharedStoreForTests: vi.fn(),
};
});
import {
getSubscriptionCache,
subscribeSubscriptionCache,
__resetSubscriptionCacheForTests,
} from "./subscriptionCache.server";
import * as cacheServer from "~/utils/cache.server";
beforeEach(() => {
__resetSubscriptionCacheForTests();
// clear shared cache.server map between tests
(cacheServer as unknown as { __test_map: Map<string, unknown> }).__test_map.clear();
vi.mocked(cacheServer.clearCacheKey).mockClear();
vi.mocked(cacheServer.clearCacheKeyPrefix).mockClear();
delete process.env.CACHE_INVALIDATION_ENABLED;
delete process.env.CACHE_L1_TTL_SECONDS;
});
afterEach(() => {
__resetSubscriptionCacheForTests();
});
describe("subscription cache (two-tier)", () => {
it("invalidate(tenantId) publishes + clears L2 and triggers local clearCacheKey via subscriber", async () => {
const cache = getSubscriptionCache();
await subscribeSubscriptionCache();
await cache.invalidate("tenant-1");
// Give the in-process pubsub event loop a tick to flush.
await new Promise((r) => setTimeout(r, 10));
expect(vi.mocked(cacheServer.clearCacheKey)).toHaveBeenCalledWith("tenantSubscription:tenant-1");
});
it("wildcard invalidate clears the cachified prefix", async () => {
const cache = getSubscriptionCache();
await subscribeSubscriptionCache();
await cache.invalidate();
await new Promise((r) => setTimeout(r, 10));
expect(vi.mocked(cacheServer.clearCacheKeyPrefix)).toHaveBeenCalledWith("tenantSubscription:");
});
it("respects CACHE_INVALIDATION_ENABLED=false (subscribe is no-op, no publish)", async () => {
process.env.CACHE_INVALIDATION_ENABLED = "false";
__resetSubscriptionCacheForTests();
const cache = getSubscriptionCache();
const dispose = await subscribeSubscriptionCache();
await cache.invalidate("tenant-2");
await new Promise((r) => setTimeout(r, 10));
// subscriber never wired because pubsub disabled → no clearCacheKey from remote path
expect(vi.mocked(cacheServer.clearCacheKey)).not.toHaveBeenCalled();
await dispose();
});
it("subscribeSubscriptionCache is idempotent (returns same disposer)", async () => {
const a = await subscribeSubscriptionCache();
const b = await subscribeSubscriptionCache();
expect(a).toBe(b);
});
it("uses CACHE_L1_TTL_SECONDS override when present", async () => {
process.env.CACHE_L1_TTL_SECONDS = "120";
__resetSubscriptionCacheForTests();
const cache = getSubscriptionCache();
// Smoke: a set/get round-trip works with overridden TTL.
await cache.set("t1", null);
expect(await cache.get("t1")).toBeNull();
});
});
// Silence the unused createSharedStore import — used only to load the package
// (and force its types) for the mock factory above. vitest's mock factory is
// hoisted so the dynamic import shape is the standard pattern here.
void MemoryAdapter;
void createSharedStore;