Ctrl K
ComunidadGuías…

test/shared/cache-registry.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
    });
  });
});