Skip to main content
Glama
CacheInvalidation.test.js18.2 kB
import { vi } from "vitest"; import { CacheInvalidation } from "@/cache/CacheInvalidation.js"; import { HttpCacheWrapper } from "@/cache/HttpCacheWrapper.js"; // Mock HttpCacheWrapper vi.mock("../../dist/cache/HttpCacheWrapper.js"); describe("CacheInvalidation", () => { let invalidation; let mockHttpCache; beforeEach(() => { mockHttpCache = { invalidatePattern: vi.fn(), invalidateKey: vi.fn(), clear: vi.fn(), getKeys: vi.fn().mockReturnValue([]), siteId: "test-site", }; HttpCacheWrapper.mockImplementation(() => mockHttpCache); invalidation = new CacheInvalidation(mockHttpCache); }); describe("constructor", () => { it("should initialize with default rules", () => { expect(invalidation).toBeDefined(); expect(invalidation.invalidationRules).toBeDefined(); expect(invalidation.eventQueue).toBeDefined(); }); it("should setup default invalidation rules", () => { // Check that default rules are registered expect(invalidation.invalidationRules.size).toBeGreaterThan(0); // Verify some expected rules exist expect(invalidation.invalidationRules.has("posts")).toBe(true); expect(invalidation.invalidationRules.has("users")).toBe(true); expect(invalidation.invalidationRules.has("comments")).toBe(true); expect(invalidation.invalidationRules.has("media")).toBe(true); }); }); describe("registerRule", () => { it("should register new invalidation rule", () => { const rule = { trigger: "create", patterns: ["custom/*"], immediate: true, cascade: false, }; invalidation.registerRule("custom", rule); const rules = invalidation.invalidationRules.get("custom"); expect(rules).toHaveLength(1); expect(rules[0]).toEqual(rule); }); it("should add multiple rules for same resource", () => { const rule1 = { trigger: "create", patterns: ["custom/*"], immediate: true, }; const rule2 = { trigger: "update", patterns: ["custom/\\d+"], immediate: false, }; invalidation.registerRule("custom", rule1); invalidation.registerRule("custom", rule2); const rules = invalidation.invalidationRules.get("custom"); expect(rules).toHaveLength(2); expect(rules[0]).toEqual(rule1); expect(rules[1]).toEqual(rule2); }); it("should handle different trigger types", () => { const createRule = { trigger: "create", patterns: ["*"] }; const updateRule = { trigger: "update", patterns: ["*"] }; const deleteRule = { trigger: "delete", patterns: ["*"] }; invalidation.registerRule("test", createRule); invalidation.registerRule("test", updateRule); invalidation.registerRule("test", deleteRule); const rules = invalidation.invalidationRules.get("test"); expect(rules).toHaveLength(3); expect(rules.map((r) => r.trigger)).toEqual(["create", "update", "delete"]); }); }); describe("trigger", () => { it("should add event to queue", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; await invalidation.trigger(event); expect(invalidation.eventQueue).toContainEqual(event); }); it("should process queue immediately if not processing", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; const processQueueSpy = vi.spyOn(invalidation, "processQueue"); await invalidation.trigger(event); expect(processQueueSpy).toHaveBeenCalled(); }); it("should not process queue if already processing", async () => { invalidation.processing = true; const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; const processQueueSpy = vi.spyOn(invalidation, "processQueue"); await invalidation.trigger(event); expect(processQueueSpy).not.toHaveBeenCalled(); expect(invalidation.eventQueue).toContainEqual(event); }); }); describe("invalidateResource", () => { it("should create and trigger invalidation event", async () => { const triggerSpy = vi.spyOn(invalidation, "trigger"); await invalidation.invalidateResource("posts", 123, "update"); expect(triggerSpy).toHaveBeenCalledWith({ type: "update", resource: "posts", id: 123, siteId: "test-site", timestamp: expect.any(Number), }); }); it("should use default type when not specified", async () => { const triggerSpy = vi.spyOn(invalidation, "trigger"); await invalidation.invalidateResource("posts", 123); expect(triggerSpy).toHaveBeenCalledWith({ type: "update", resource: "posts", id: 123, siteId: "test-site", timestamp: expect.any(Number), }); }); it("should handle resource without ID", async () => { const triggerSpy = vi.spyOn(invalidation, "trigger"); await invalidation.invalidateResource("posts", undefined, "create"); expect(triggerSpy).toHaveBeenCalledWith({ type: "create", resource: "posts", id: undefined, siteId: "test-site", timestamp: expect.any(Number), }); }); }); describe("processQueue", () => { it("should process events in queue", async () => { const event1 = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; const event2 = { type: "update", resource: "posts", id: 124, siteId: "test-site", timestamp: Date.now(), }; invalidation.eventQueue = [event1, event2]; const processEventSpy = vi.spyOn(invalidation, "processEvent"); await invalidation.processQueue(); expect(processEventSpy).toHaveBeenCalledWith(event1); expect(processEventSpy).toHaveBeenCalledWith(event2); expect(invalidation.eventQueue).toHaveLength(0); }); it("should set processing flag during queue processing", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; invalidation.eventQueue = [event]; let processingDuringExecution = false; vi.spyOn(invalidation, "processEvent").mockImplementation(async () => { processingDuringExecution = invalidation.processing; return Promise.resolve(); }); await invalidation.processQueue(); expect(processingDuringExecution).toBe(true); expect(invalidation.processing).toBe(false); }); it("should handle errors during processing", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; invalidation.eventQueue = [event]; vi.spyOn(invalidation, "processEvent").mockRejectedValue(new Error("Process error")); await expect(invalidation.processQueue()).resolves.not.toThrow(); expect(invalidation.processing).toBe(false); }); }); describe("processEvent", () => { it("should apply matching rules for event", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; const applyRuleSpy = vi.spyOn(invalidation, "applyRule"); await invalidation.processEvent(event); expect(applyRuleSpy).toHaveBeenCalled(); }); it("should skip rules that don't match trigger", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; // Add a rule that doesn't match invalidation.registerRule("posts", { trigger: "delete", patterns: ["posts/*"], }); const applyRuleSpy = vi.spyOn(invalidation, "applyRule"); await invalidation.processEvent(event); // Should not call applyRule for mismatched trigger expect(applyRuleSpy).not.toHaveBeenCalledWith(expect.objectContaining({ trigger: "delete" }), event); }); it("should handle event with no matching rules", async () => { const event = { type: "create", resource: "unknown", id: 123, siteId: "test-site", timestamp: Date.now(), }; await expect(invalidation.processEvent(event)).resolves.not.toThrow(); }); }); describe("applyRule", () => { it("should invalidate cache patterns from rule", async () => { const rule = { trigger: "create", patterns: ["posts/*", "posts/\\d+"], immediate: true, }; const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; await invalidation.applyRule(rule, event); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("posts/*"); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("posts/\\d+"); }); it("should handle immediate invalidation", async () => { const rule = { trigger: "create", patterns: ["posts/*"], immediate: true, }; const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; await invalidation.applyRule(rule, event); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("posts/*"); }); it("should handle cascading invalidation", async () => { const rule = { trigger: "create", patterns: ["posts/*"], cascade: true, }; const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; // Mock related patterns mockHttpCache.getKeys.mockReturnValue(["posts/123", "categories/1", "tags/2"]); await invalidation.applyRule(rule, event); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("posts/*"); }); it("should substitute patterns with event data", async () => { const rule = { trigger: "update", patterns: ["posts/{id}", "categories/{category}"], immediate: true, }; const event = { type: "update", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), data: { category: 5 }, }; await invalidation.applyRule(rule, event); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("posts/123"); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledWith("categories/5"); }); }); describe("default rules", () => { it("should have post invalidation rules", () => { const postRules = invalidation.invalidationRules.get("posts"); expect(postRules).toBeDefined(); expect(postRules.length).toBeGreaterThan(0); const createRule = postRules.find((r) => r.trigger === "create"); expect(createRule).toBeDefined(); expect(createRule.patterns).toContain("posts"); expect(createRule.immediate).toBe(true); expect(createRule.cascade).toBe(true); }); it("should have user invalidation rules", () => { const userRules = invalidation.invalidationRules.get("users"); expect(userRules).toBeDefined(); expect(userRules.length).toBeGreaterThan(0); const updateRule = userRules.find((r) => r.trigger === "update"); expect(updateRule).toBeDefined(); expect(updateRule.patterns).toContain("users/\\d+"); }); it("should have comment invalidation rules", () => { const commentRules = invalidation.invalidationRules.get("comments"); expect(commentRules).toBeDefined(); expect(commentRules.length).toBeGreaterThan(0); const createRule = commentRules.find((r) => r.trigger === "create"); expect(createRule).toBeDefined(); expect(createRule.patterns).toContain("comments"); }); it("should have media invalidation rules", () => { const mediaRules = invalidation.invalidationRules.get("media"); expect(mediaRules).toBeDefined(); expect(mediaRules.length).toBeGreaterThan(0); const deleteRule = mediaRules.find((r) => r.trigger === "delete"); expect(deleteRule).toBeDefined(); expect(deleteRule.patterns).toContain("media"); }); }); describe("pattern matching", () => { it("should match simple patterns", () => { expect(invalidation.matchPattern("posts", "posts")).toBe(true); expect(invalidation.matchPattern("posts/123", "posts/*")).toBe(true); expect(invalidation.matchPattern("posts/123", "posts/\\d+")).toBe(true); expect(invalidation.matchPattern("posts/abc", "posts/\\d+")).toBe(false); }); it("should match regex patterns", () => { expect(invalidation.matchPattern("posts/123", "posts/\\d+")).toBe(true); expect(invalidation.matchPattern("posts/123/comments", "posts/\\d+/comments")).toBe(true); expect(invalidation.matchPattern("posts/abc", "posts/\\d+")).toBe(false); }); it("should handle wildcard patterns", () => { expect(invalidation.matchPattern("posts/123", "posts/*")).toBe(true); expect(invalidation.matchPattern("posts/123/comments", "posts/*")).toBe(true); expect(invalidation.matchPattern("users/123", "posts/*")).toBe(false); }); it("should handle complex patterns", () => { expect(invalidation.matchPattern("posts/123?page=2", "posts/\\d+.*")).toBe(true); expect(invalidation.matchPattern("posts/123/comments?filter=approved", "posts/\\d+/comments.*")).toBe(true); }); }); describe("batch invalidation", () => { it("should process multiple events in batch", async () => { const events = [ { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }, { type: "update", resource: "posts", id: 124, siteId: "test-site", timestamp: Date.now(), }, ]; await invalidation.batchInvalidate(events); expect(mockHttpCache.invalidatePattern).toHaveBeenCalledTimes(6); // 3 patterns per event }); it("should deduplicate patterns in batch", async () => { const events = [ { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }, { type: "create", resource: "posts", id: 124, siteId: "test-site", timestamp: Date.now(), }, ]; await invalidation.batchInvalidate(events); // Should not duplicate pattern invalidations const callCount = mockHttpCache.invalidatePattern.mock.calls.length; const uniquePatterns = new Set(mockHttpCache.invalidatePattern.mock.calls.map((call) => call[0])); expect(uniquePatterns.size).toBeLessThanOrEqual(callCount); }); }); describe("error handling", () => { it("should handle invalidation errors gracefully", async () => { mockHttpCache.invalidatePattern.mockRejectedValue(new Error("Cache error")); const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; await expect(invalidation.processEvent(event)).resolves.not.toThrow(); }); it("should handle malformed events", async () => { const malformedEvent = { type: "invalid", resource: null, siteId: "test-site", timestamp: Date.now(), }; await expect(invalidation.processEvent(malformedEvent)).resolves.not.toThrow(); }); it("should handle empty rule patterns", async () => { const rule = { trigger: "create", patterns: [], immediate: true, }; const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; await expect(invalidation.applyRule(rule, event)).resolves.not.toThrow(); expect(mockHttpCache.invalidatePattern).not.toHaveBeenCalled(); }); }); describe("performance", () => { it("should handle high volume events efficiently", async () => { const events = []; for (let i = 0; i < 1000; i++) { events.push({ type: "create", resource: "posts", id: i, siteId: "test-site", timestamp: Date.now(), }); } const startTime = Date.now(); for (const event of events) { await invalidation.trigger(event); } const endTime = Date.now(); const duration = endTime - startTime; // Should process 1000 events in reasonable time (< 1 second) expect(duration).toBeLessThan(1000); }); it("should not block on queue processing", async () => { const event = { type: "create", resource: "posts", id: 123, siteId: "test-site", timestamp: Date.now(), }; // Mock slow processing vi.spyOn(invalidation, "processEvent").mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 100)); }); const startTime = Date.now(); // Trigger should return immediately await invalidation.trigger(event); const endTime = Date.now(); const duration = endTime - startTime; // Should not wait for processing to complete expect(duration).toBeLessThan(50); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server