test/shared/cache-registry.test.ts
Rama main · réplica de lectura del repositorio original.
import { expect } from "@std/expect";
import { afterEach, describe, it as test } from "@std/testing/bdd";
import {
getAllCacheStats,
invalidateCachesForTable,
invalidateCachesForWrite,
registerCache,
registerCacheReset,
registerDependencies,
registerTableInvalidation,
resetAllCaches,
type Unregister,
} from "#shared/cache-registry.ts";
describe("cache-registry", () => {
// The registry is shared module state (app caches register at load time), so
// every registration this suite makes is tracked and removed after each test
// rather than wiping the whole registry out from under the rest of the app.
const cleanups: Unregister[] = [];
const track = (unregister: Unregister): void => {
cleanups.push(unregister);
};
afterEach(() => {
for (const cleanup of cleanups.splice(0)) cleanup();
});
/** Only the stats this suite registered, by their unique test names. */
const ownStats = (...names: string[]): { name: string; entries: number }[] =>
getAllCacheStats()
.filter((stat) => names.includes(stat.name))
.map((stat) => ({ entries: stat.entries, name: stat.name }));
describe("registerCache / getAllCacheStats", () => {
test("collects a single registered cache's stat", () => {
track(registerCache(() => ({ entries: 3, name: "registry-widgets" })));
expect(ownStats("registry-widgets")).toEqual([
{ entries: 3, name: "registry-widgets" },
]);
});
test("collects every registered cache's stat, in registration order", () => {
track(registerCache(() => ({ entries: 1, name: "registry-a" })));
track(registerCache(() => ({ entries: 2, name: "registry-b" })));
expect(ownStats("registry-a", "registry-b")).toEqual([
{ entries: 1, name: "registry-a" },
{ entries: 2, name: "registry-b" },
]);
});
test("calls each provider fresh on every call, not a cached snapshot", () => {
let entries = 1;
track(registerCache(() => ({ entries, name: "registry-widgets" })));
expect(ownStats("registry-widgets")).toEqual([
{ entries: 1, name: "registry-widgets" },
]);
entries = 2;
expect(ownStats("registry-widgets")).toEqual([
{ entries: 2, name: "registry-widgets" },
]);
});
test("an unregistered provider no longer appears in the stats", () => {
const unregister = registerCache(() => ({
entries: 1,
name: "registry-gone",
}));
unregister();
expect(ownStats("registry-gone")).toEqual([]);
});
});
describe("registerTableInvalidation / invalidateCachesForWrite", () => {
test("does not fire for a table with no registrations", () => {
expect(() =>
invalidateCachesForWrite("untouched", { updatedColumns: null }),
).not.toThrow();
});
test("fires the invalidator when its exact table is written", () => {
let calls = 0;
track(
registerTableInvalidation(["listings"], () => {
calls++;
}),
);
invalidateCachesForWrite("listings", { updatedColumns: null });
expect(calls).toBe(1);
});
test("does not fire for a different table", () => {
let calls = 0;
track(
registerTableInvalidation(["listings"], () => {
calls++;
}),
);
invalidateCachesForWrite("attendees", { updatedColumns: null });
expect(calls).toBe(0);
});
test("an unregistered invalidator no longer fires", () => {
let calls = 0;
const unregister = registerTableInvalidation(
["listings", "attendees"],
() => {
calls++;
},
);
unregister();
invalidateCachesForWrite("listings", { updatedColumns: null });
invalidateCachesForWrite("attendees", { updatedColumns: null });
expect(calls).toBe(0);
});
test("registers the same invalidator against every listed table", () => {
let calls = 0;
track(
registerTableInvalidation(["listings", "attendees"], () => {
calls++;
}),
);
invalidateCachesForWrite("listings", { updatedColumns: null });
invalidateCachesForWrite("attendees", { updatedColumns: null });
expect(calls).toBe(2);
});
test("fires every independently-registered invalidator for the same table", () => {
let firstCalls = 0;
let secondCalls = 0;
track(
registerTableInvalidation(["listings"], () => {
firstCalls++;
}),
);
track(
registerTableInvalidation(["listings"], () => {
secondCalls++;
}),
);
invalidateCachesForWrite("listings", { updatedColumns: null });
expect(firstCalls).toBe(1);
expect(secondCalls).toBe(1);
});
describe("whenColumns gating", () => {
/** Registers a gated invalidator on "listings" and returns how many
* times it fired for a single update touching `updatedColumns`. */
const callsForGatedUpdate = (
whenColumns: readonly string[],
assigned: readonly string[],
): number => {
let calls = 0;
track(
registerTableInvalidation(
["listings"],
() => {
calls++;
},
{ whenColumns },
),
);
invalidateCachesForWrite("listings", {
updatedColumns: new Set(assigned),
});
return calls;
};
test("an ungated dependency fires on update regardless of assigned columns", () => {
let calls = 0;
track(
registerTableInvalidation(["listings"], () => {
calls++;
}),
);
invalidateCachesForWrite("listings", {
updatedColumns: new Set(["unrelated_column"]),
});
expect(calls).toBe(1);
});
test("a gated dependency fires on update only when an assigned column is listed", () => {
expect(callsForGatedUpdate(["price", "name"], ["name"])).toBe(1);
expect(callsForGatedUpdate(["price", "name"], ["description"])).toBe(0);
});
test("an empty whenColumns list gates out every update, since no column can ever match", () => {
expect(callsForGatedUpdate([], ["name"])).toBe(0);
});
test("a gated dependency always fires on a write that assigns nothing", () => {
// No assigned columns means the write is not an UPDATE the gate can
// narrow — a row entering or leaving always shifts the aggregates.
let calls = 0;
track(
registerTableInvalidation(
["listings"],
() => {
calls++;
},
{ whenColumns: ["price"] },
),
);
invalidateCachesForWrite("listings", { updatedColumns: null });
expect(calls).toBe(1);
});
});
});
describe("invalidateCachesForTable", () => {
test("fires an ungated invalidator", () => {
let calls = 0;
track(
registerTableInvalidation(["listings"], () => {
calls++;
}),
);
invalidateCachesForTable("listings");
expect(calls).toBe(1);
});
test("fires a column-gated invalidator too, treating the write as unconditional (INSERT semantics)", () => {
let calls = 0;
track(
registerTableInvalidation(
["listings"],
() => {
calls++;
},
{ whenColumns: ["price"] },
),
);
invalidateCachesForTable("listings");
expect(calls).toBe(1);
});
test("is a no-op for a table with no registrations", () => {
expect(() => invalidateCachesForTable("untouched")).not.toThrow();
});
});
describe("registerDependencies", () => {
test("registers the own table unconditionally, even for an update touching unrelated columns", () => {
let calls = 0;
track(
registerDependencies("listings", [], () => {
calls++;
}),
);
invalidateCachesForWrite("listings", {
updatedColumns: new Set(["unrelated"]),
});
expect(calls).toBe(1);
});
test("registers a plain string dependency unconditionally", () => {
let calls = 0;
track(
registerDependencies("listings", ["listing_attendees"], () => {
calls++;
}),
);
invalidateCachesForWrite("listing_attendees", {
updatedColumns: new Set(["unrelated"]),
});
expect(calls).toBe(1);
});
test("gates an object dependency's whenColumns on update", () => {
let calls = 0;
track(
registerDependencies(
"listings",
[{ table: "listing_prices", whenColumns: ["amount"] }],
() => {
calls++;
},
),
);
invalidateCachesForWrite("listing_prices", {
updatedColumns: new Set(["unrelated"]),
});
expect(calls).toBe(0);
invalidateCachesForWrite("listing_prices", {
updatedColumns: new Set(["amount"]),
});
expect(calls).toBe(1);
});
test("fires once per independently-written table, not once overall", () => {
let calls = 0;
track(
registerDependencies("listings", ["listing_attendees"], () => {
calls++;
}),
);
invalidateCachesForTable("listings");
invalidateCachesForTable("listing_attendees");
expect(calls).toBe(2);
});
test("unregistering removes the own-table and every dependency registration", () => {
let calls = 0;
const unregister = registerDependencies(
"listings",
[
"listing_attendees",
{ table: "listing_prices", whenColumns: ["amount"] },
],
() => {
calls++;
},
);
unregister();
invalidateCachesForTable("listings");
invalidateCachesForTable("listing_attendees");
invalidateCachesForWrite("listing_prices", {
updatedColumns: new Set(["amount"]),
});
expect(calls).toBe(0);
});
});
describe("resetAllCaches", () => {
test("fires every table-registered invalidator and every reset hook", () => {
let tableCalls = 0;
const hookCauses: string[] = [];
track(
registerTableInvalidation(["listings"], () => {
tableCalls++;
}),
);
track(
registerCacheReset((cause = "manual") => {
hookCauses.push(cause);
}),
);
resetAllCaches();
expect(tableCalls).toBe(1);
expect(hookCauses).toEqual(["write"]);
});
test("fires an invalidator registered against several tables only once", () => {
let calls = 0;
track(
registerTableInvalidation(["listings", "listing_attendees"], () => {
calls++;
}),
);
resetAllCaches();
expect(calls).toBe(1);
});
test("fires a column-gated invalidator too — a full reset ignores gates", () => {
let calls = 0;
track(
registerTableInvalidation(
["listings"],
() => {
calls++;
},
{ whenColumns: ["name"] },
),
);
resetAllCaches();
expect(calls).toBe(1);
});
test("an unregistered reset hook no longer fires", () => {
let calls = 0;
const unregister = registerCacheReset(() => {
calls++;
});
unregister();
resetAllCaches();
expect(calls).toBe(0);
});
});
});