@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
367 lines (304 loc) • 12.1 kB
text/typescript
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import type { ResponseSuccessType } from "adapter";
import type { ResponseDetailsType } from "managers";
import { createCache } from "../../utils";
import { Client } from "client";
import type { HttpAdapterType } from "http-adapter";
import { xhrExtra } from "http-adapter";
const { resetMocks, startServer, stopServer } = createHttpMockingServer();
describe("Cache [ Base ]", () => {
const response: ResponseSuccessType<unknown, HttpAdapterType> = {
data: 123,
error: null,
status: 200,
success: true,
extra: xhrExtra,
requestTimestamp: Date.now(),
responseTimestamp: Date.now(),
};
const details: ResponseDetailsType = {
retries: 0,
requestTimestamp: +new Date(),
responseTimestamp: +new Date(),
addedTimestamp: +new Date(),
triggerTimestamp: +new Date(),
isCanceled: false,
isOffline: false,
willRetry: false,
};
let client = new Client({ url: "shared-base-url" });
let request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint" });
let cache = createCache(client);
beforeAll(() => {
startServer();
});
beforeEach(() => {
client = new Client({ url: "shared-base-url" });
request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint" });
cache = createCache(client);
resetMocks();
vi.resetAllMocks();
});
afterAll(() => {
stopServer();
});
describe("When lifecycle events get triggered", () => {
it("should initialize cache", async () => {
expect(cache.get(request.cacheKey)).not.toBeDefined();
cache.set(request.setCache(true), { ...response, ...details, data: "TEST!" });
expect(cache.get(request.cacheKey)).toBeDefined();
expect(cache.get(request.cacheKey)?.data).toBe("TEST!");
cache.set(request.setCache(true), (previous) => ({ ...response, ...details, data: `${previous?.data} WOW!` }));
expect(cache.get(request.cacheKey)?.data).toBe("TEST! WOW!");
});
it("should update cache", async () => {
cache.set(request.setCache(true), { ...response, ...details });
cache.update(request.setCache(true), { ...response, ...details, data: "SUPER TEST!" });
expect(cache.get(request.cacheKey)?.data).toBe("SUPER TEST!");
cache.update(request.setCache(true), (previous) => ({ ...response, ...details, data: `${previous?.data} WOW!` }));
expect(cache.get(request.cacheKey)?.data).toBe("SUPER TEST! WOW!");
});
});
describe("When managing cache data", () => {
it("should add element to cache and emit set event", async () => {
const trigger = vi.fn();
const unmount = cache.events.onDataByKey(request.cacheKey, trigger);
cache.set(request.setCache(true), { ...response, ...details });
unmount();
expect(trigger).toHaveBeenCalledTimes(1);
expect(cache.get(request.cacheKey)).toBeDefined();
});
it("should delete cache", async () => {
cache.set(request.setCache(true), { ...response, ...details });
cache.delete(request.cacheKey);
await sleep(1);
expect(cache.get(request.cacheKey)).not.toBeDefined();
});
it("should invalidate and remove cache", async () => {
const trigger = vi.fn();
const unmount = cache.events.onInvalidateByKey(request.cacheKey, trigger);
cache.set(request.setCache(true), { ...response, ...details });
await cache.invalidate(request.cacheKey);
await sleep(1);
const data = cache.get(request.cacheKey);
expect(data).toBeDefined();
expect(data?.staleTime).toBe(0);
expect(trigger).toHaveBeenCalledTimes(1);
unmount();
});
it("should not add to cache when useCache is set to false", async () => {
const trigger = vi.fn();
const unmount = cache.events.onDataByKey(request.cacheKey, trigger);
cache.set(request.setCache(false), { ...response, ...details });
unmount();
expect(trigger).toHaveBeenCalledTimes(1);
expect(cache.get(request.cacheKey)).not.toBeDefined();
});
it("should allow to get cache keys", async () => {
cache.set(request.setCacheKey("1"), { ...response, ...details });
cache.set(request.setCacheKey("2"), { ...response, ...details });
cache.set(request.setCacheKey("3"), { ...response, ...details });
expect(cache.keys()).toHaveLength(3);
expect(cache.keys()).toStrictEqual(["1", "2", "3"]);
});
it("should handle both function and non-function responses", async () => {
// Test direct value response
cache.set(request.setCache(true), { ...response, ...details, data: "DIRECT" });
expect(cache.get(request.cacheKey)?.data).toBe("DIRECT");
// Test function response with no previous data
cache.delete(request.cacheKey); // Clear previous data
cache.set(request.setCache(true), () => ({ ...response, ...details, data: "FUNCTION NO PREV" }));
expect(cache.get(request.cacheKey)?.data).toBe("FUNCTION NO PREV");
// Test function response with previous data
cache.set(request.setCache(true), (prev) => ({
...response,
...details,
data: `${prev?.data} UPDATED`,
}));
expect(cache.get(request.cacheKey)?.data).toBe("FUNCTION NO PREV UPDATED");
});
it("should properly merge partial responses with existing cache data", async () => {
// Set initial cache data
const initialData = {
...response,
...details,
data: { field1: "value1", field2: "value2" },
};
cache.set(request.setCache(true), initialData);
// Test direct value partial update
cache.update(request.setCache(true), { data: { field2: "updated2", field3: "value3" } });
let result = cache.get(request.cacheKey);
expect(result?.data).toEqual({
field2: "updated2",
field3: "value3",
});
cache.delete(request.cacheKey);
// Cannot update if cache is not set
cache.update(request.setCache(true), ((prev: any) => {
expect(prev).toBeNull();
return {
...prev,
data: {
...prev?.data,
field1: "updated1",
field4: "value4",
},
};
}) as any);
result = cache.get(request.cacheKey);
expect(result).toBeUndefined();
});
});
describe("When CacheStore gets cleared before triggering cache actions", () => {
it("should return undefined when removed cache entity", async () => {
const trigger = vi.fn();
cache.events.onInvalidateByKey(request.cacheKey, trigger);
cache.set(request.setCache(false), { ...response, ...details });
cache.clear();
expect(trigger).toHaveBeenCalledTimes(0);
expect(cache.get(request.cacheKey)).not.toBeDefined();
});
});
describe("invalidate", () => {
it("should set staleTime to 0 when invalidating with existing cache", async () => {
// Set initial cache data
const initialData = {
...response,
...details,
data: "initial",
staleTime: 1000,
};
cache.set(request.setCache(true), initialData);
// Verify initial staleTime
expect(cache.get(request.cacheKey)?.staleTime).toBeGreaterThan(0);
// Update the value
await cache.invalidate(request.cacheKey);
// Verify staleTime is set to 0
const updatedCache = cache.get(request.cacheKey);
expect(updatedCache?.staleTime).toBe(0);
});
it("should not set staleTime to 0 when invalidating with non-existing cache", async () => {
cache.storage.set(request.cacheKey, undefined as any);
// Update the value
await cache.invalidate(request.cacheKey);
// Verify staleTime is set to 0
const updatedCache = cache.get(request.cacheKey);
expect(updatedCache).toBeUndefined();
});
it("should invalidate cache when given a Request instance", async () => {
// Create a mock Request instance
const mockRequest = client.createRequest()({ endpoint: "/shared-endpoint" });
// Add some data to cache with the request's cacheKey
const data = {
data: "test",
success: true,
staleTime: 1000,
version: "0.0.1",
cacheKey: mockRequest.cacheKey,
scope: null,
cacheTime: 2000,
responseTimestamp: Date.now(),
error: null,
status: 200,
extra: xhrExtra,
requestTimestamp: Date.now(),
retries: 0,
isCanceled: false,
isOffline: false,
willRetry: false,
addedTimestamp: Date.now(),
triggerTimestamp: Date.now(),
cached: true,
};
cache.storage.set(mockRequest.cacheKey, data);
await cache.invalidate(mockRequest);
const invalidatedData = cache.storage.get(mockRequest.cacheKey);
expect(invalidatedData?.staleTime).toBe(0);
});
it("should not invalidate cache when it is empty", async () => {
// Create a mock Request instance
const mockRequest = client
.createRequest()({ endpoint: "/shared-endpoint" })
.setMock(() => ({
data: "test" as any,
status: 200,
}));
cache.invalidate([new RegExp(mockRequest.cacheKey)]);
const spy = vi.spyOn(cache.events, "emitInvalidation");
expect(spy).not.toHaveBeenCalled();
});
it("should not invalidate cache when app is offline", async () => {
// Create a mock Request instance
const mockRequest = client
.createRequest()({ endpoint: "/shared-endpoint", cacheTime: -100 })
.setMock(() => ({
data: "test" as any,
status: 200,
}));
await mockRequest.send({
onResponse: () => {
client.appManager.setOnline(false);
},
});
cache.garbageCollectors.clear();
cache.invalidate([new RegExp(mockRequest.cacheKey)]);
const spy = vi.spyOn(cache.events, "emitInvalidation");
expect(spy).not.toHaveBeenCalled();
});
it("should not invalidate cache when app goes offline", async () => {
// Create a mock Request instance
const mockRequest = client
.createRequest()({ endpoint: "/shared-endpoint", cacheTime: 100 })
.setMock(() => ({
data: "test" as any,
status: 200,
}));
await mockRequest.send();
cache.invalidate([new RegExp(mockRequest.cacheKey)]);
client.appManager.setOnline(false);
await sleep(150);
const spy = vi.spyOn(cache.events, "emitInvalidation");
expect(spy).not.toHaveBeenCalled();
});
it("should handle array of mixed invalidation keys (Request, string, RegExp)", async () => {
// Create test data with different keys
const mockRequest = client.createRequest()({ endpoint: "/shared-endpoint" });
const testData = {
[mockRequest.cacheKey]: "request data",
"string-key": "string data",
"test-prefix-123": "regex data 1",
"test-prefix-456": "regex data 2",
};
// Set up cache with test data
Object.entries(testData).forEach(([key, value]) => {
cache.storage.set(key, {
data: value,
success: true,
staleTime: 1000,
version: "0.0.1",
cacheKey: key,
scope: null,
cacheTime: 2000,
responseTimestamp: Date.now(),
error: null,
status: 200,
extra: xhrExtra,
requestTimestamp: Date.now(),
retries: 0,
isCanceled: false,
isOffline: false,
willRetry: false,
addedTimestamp: Date.now(),
triggerTimestamp: Date.now(),
cached: true,
});
});
// Invalidate using array of different key types
await cache.invalidate([mockRequest, "string-key", /^test-prefix-/]);
// Verify all matching keys were invalidated
Object.entries(testData).forEach(([key]) => {
expect(cache.storage.get(key)?.staleTime).toBe(0);
});
});
});
});