release: WRNexusJS 0.8.0
This commit is contained in:
Vendored
+104
-1
@@ -1,7 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TagCache, responseCache } from "../src/index.ts";
|
||||
import {
|
||||
CacheCoordinator,
|
||||
connectCacheInvalidation,
|
||||
RequestCache,
|
||||
TagCache,
|
||||
responseCache,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("@wrnexus/cache", () => {
|
||||
test("separates request, data, component, and page cache lifetimes", async () => {
|
||||
const coordinator = new CacheCoordinator({ ttlMs: 100, eventLimit: 10 });
|
||||
coordinator.data.set("user:1", { id: 1 }, { tags: ["users"] });
|
||||
coordinator.component.set("avatar:1", "html", { tags: ["users"] });
|
||||
coordinator.page.set("/users", "document", { tags: ["users"] });
|
||||
expect(coordinator.inspect().layers.data[0]?.key).toBe("user:1");
|
||||
expect(coordinator.invalidateTags(["users"])).toBe(3);
|
||||
expect(
|
||||
coordinator.inspect().recentEvents.some((event) => event.operation === "invalidate"),
|
||||
).toBe(true);
|
||||
|
||||
const request = new RequestCache();
|
||||
let calls = 0;
|
||||
const [first, second] = await Promise.all([
|
||||
request.getOrLoad("permissions", async () => ++calls),
|
||||
request.getOrLoad("permissions", async () => ++calls),
|
||||
]);
|
||||
expect([first, second, calls]).toEqual([1, 1, 1]);
|
||||
});
|
||||
test("supports fresh, stale, and tag invalidation", async () => {
|
||||
let now = 0;
|
||||
const cache = new TagCache<number>({ ttlMs: 10, staleWhileRevalidateMs: 10, clock: () => now });
|
||||
@@ -23,6 +48,84 @@ describe("@wrnexus/cache", () => {
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
test("provides exclusive per-key locks", async () => {
|
||||
const cache = new TagCache();
|
||||
const order: string[] = [];
|
||||
let release!: () => void;
|
||||
const first = cache.withLock("catalog", async () => {
|
||||
order.push("first:start");
|
||||
await new Promise<void>((resolve) => (release = resolve));
|
||||
order.push("first:end");
|
||||
});
|
||||
await Promise.resolve();
|
||||
const second = cache.withLock("catalog", () => order.push("second"));
|
||||
await Promise.resolve();
|
||||
expect(order).toEqual(["first:start"]);
|
||||
release();
|
||||
await Promise.all([first, second]);
|
||||
expect(order).toEqual(["first:start", "first:end", "second"]);
|
||||
});
|
||||
|
||||
test("invalidation during a load prevents stale work from repopulating the cache", async () => {
|
||||
const cache = new TagCache<number>();
|
||||
let release!: (value: number) => void;
|
||||
const loading = cache.getOrLoad(
|
||||
"x",
|
||||
() => new Promise<number>((resolve) => (release = resolve)),
|
||||
);
|
||||
await Promise.resolve();
|
||||
cache.clear();
|
||||
release(7);
|
||||
expect(await loading).toBe(7);
|
||||
expect(cache.lookup("x").state).toBe("miss");
|
||||
});
|
||||
|
||||
test("propagates tag, key, and clear invalidations across instances", async () => {
|
||||
const subscriptions = new Map<string, Set<(message: unknown) => void | Promise<void>>>();
|
||||
const bus = {
|
||||
async publish(topic: string, message: unknown) {
|
||||
await Promise.all(
|
||||
[...(subscriptions.get(topic) ?? [])].map((handler) => Promise.resolve(handler(message))),
|
||||
);
|
||||
},
|
||||
subscribe(topic: string, handler: (message: unknown) => void | Promise<void>) {
|
||||
const handlers = subscriptions.get(topic) ?? new Set();
|
||||
handlers.add(handler);
|
||||
subscriptions.set(topic, handlers);
|
||||
return () => handlers.delete(handler);
|
||||
},
|
||||
};
|
||||
const first = new TagCache<number>();
|
||||
const second = new TagCache<number>();
|
||||
const firstChannel = connectCacheInvalidation(first, bus, {
|
||||
namespace: "catalog",
|
||||
instanceId: "one",
|
||||
});
|
||||
const secondChannel = connectCacheInvalidation(second, bus, {
|
||||
namespace: "catalog",
|
||||
instanceId: "two",
|
||||
});
|
||||
for (const cache of [first, second]) cache.set("product:1", 1, { tags: ["products"] });
|
||||
|
||||
expect(await firstChannel.invalidateTag("products")).toBe(1);
|
||||
expect(second.lookup("product:1").state).toBe("miss");
|
||||
first.set("one", 1);
|
||||
second.set("one", 1);
|
||||
await secondChannel.delete("one");
|
||||
expect(first.lookup("one").state).toBe("miss");
|
||||
first.set("all", 1);
|
||||
second.set("all", 1);
|
||||
await firstChannel.clear();
|
||||
expect(second.size).toBe(0);
|
||||
|
||||
firstChannel.close();
|
||||
firstChannel.close();
|
||||
await expect(firstChannel.invalidateTag("products")).rejects.toThrow(
|
||||
"WRN-CACHE-INVALIDATION-CLOSED",
|
||||
);
|
||||
secondChannel.close();
|
||||
});
|
||||
|
||||
test("response middleware does not call next twice for stale entries", async () => {
|
||||
let now = 0;
|
||||
const cache = new TagCache<any>({ ttlMs: 1, staleWhileRevalidateMs: 100, clock: () => now });
|
||||
|
||||
Reference in New Issue
Block a user