100 lines
3.7 KiB
TypeScript
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;
|