@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
237 lines (190 loc) • 8.91 kB
text/typescript
import { Client } from "client";
import type { CacheValueType } from "cache";
import { xhrExtra } from "http-adapter";
import { scopeKey } from "request";
describe("Client [ Hydration ]", () => {
let client = new Client({ url: "shared-base-url" });
let request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });
beforeEach(() => {
client = new Client({ url: "shared-base-url" });
request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });
});
describe("When using hydrate", () => {
it("should not dehydrate from empty cache", async () => {
const dehydratedResponse = request.dehydrate();
expect(dehydratedResponse).toBeUndefined();
});
it("should dehydrate from cache", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
const response = await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate();
expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));
const newClient = new Client({ url: "shared-base-url" });
expect(newClient.cache.get(mockedRequest.cacheKey)).not.toBeDefined();
newClient.hydrate([dehydratedResponse]);
expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual(
expect.objectContaining({ ...response, cacheKey: request.cacheKey, staleTime: request.staleTime }),
);
});
it("should dehydrate from response", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
const response = await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate({ response });
expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));
const newClient = new Client({ url: "shared-base-url" });
expect(newClient.cache.get(mockedRequest.cacheKey)).not.toBeDefined();
newClient.hydrate([dehydratedResponse]);
expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual(
expect.objectContaining({ ...response, cacheKey: request.cacheKey, staleTime: request.staleTime }),
);
});
it("should not override cache data", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
const response = await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate({ response, override: false });
expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));
const newClient = new Client({ url: "shared-base-url" });
const cacheData = {
data: "123456789",
status: 200,
error: null,
success: true,
extra: {},
responseTimestamp: +new Date(),
requestTimestamp: +new Date(),
addedTimestamp: +new Date(),
triggerTimestamp: +new Date(),
retries: 0,
isCanceled: false,
isOffline: false,
hydrated: false,
cached: true,
} as CacheValueType;
newClient.cache.set(mockedRequest, cacheData);
expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual({
...cacheData,
cacheKey: mockedRequest.cacheKey,
cacheTime: mockedRequest.cacheTime,
staleTime: mockedRequest.staleTime,
version: newClient.cache.version,
scope: null,
});
newClient.hydrate([dehydratedResponse]);
expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual({
...cacheData,
cacheKey: mockedRequest.cacheKey,
cacheTime: mockedRequest.cacheTime,
staleTime: mockedRequest.staleTime,
version: newClient.cache.version,
scope: null,
});
});
it("should handle null/undefined items in hydration data", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate();
const newClient = new Client({ url: "shared-base-url" });
// Should not throw when processing null/undefined items
newClient.hydrate([dehydratedResponse, null, undefined]);
expect(newClient.cache.get(mockedRequest.cacheKey)).toBeDefined();
});
it("should apply function-based options", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate();
const newClient = new Client({ url: "shared-base-url" });
const optionsFn = vi.fn().mockReturnValue({ cache: false });
newClient.hydrate([dehydratedResponse], optionsFn);
expect(optionsFn).toHaveBeenCalledWith(dehydratedResponse);
// Since cache is false, data should not be in cache
expect(newClient.cache.get(mockedRequest.cacheKey)).toBeUndefined();
});
it("should merge options correctly", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
const response = await mockedRequest.send();
// Test with fallback options in dehydrated response
const dehydratedResponse = mockedRequest.dehydrate({
response,
});
const newClient = new Client({ url: "shared-base-url" });
// Add global options
newClient.hydrate([dehydratedResponse], {
staleTime: 3000,
cacheTime: 2000,
override: true,
});
const cachedData = newClient.cache.get(mockedRequest.cacheKey);
expect(cachedData).toBeDefined();
expect(cachedData?.staleTime).toBe(3000);
expect(cachedData?.cacheTime).toBe(2000);
});
it("should respect cache option", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate();
const newClient = new Client({ url: "shared-base-url" });
newClient.hydrate([dehydratedResponse], { cache: false });
expect(newClient.cache.get(mockedRequest.cacheKey)).toBeUndefined();
});
it("should override if override is false and cache is not set", async () => {
const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
await mockedRequest.send();
const dehydratedResponse = mockedRequest.dehydrate();
client.cache.clear();
const spy = vi.spyOn(client.cache, "set");
client.hydrate([dehydratedResponse], { override: false });
expect(spy).toHaveBeenCalled();
});
it("should hydrate and read scoped cache via scopeKey", async () => {
const scoped = request.setScope("hydr-scope").setMock(() => ({ data: "scoped-val", status: 200 }));
const response = await scoped.send();
const dehydratedResponse = scoped.dehydrate();
expect(dehydratedResponse).toEqual(expect.objectContaining({ scope: "hydr-scope", cacheKey: scoped.cacheKey }));
const newClient = new Client({ url: "shared-base-url" });
const sk = scopeKey(scoped.cacheKey, "hydr-scope");
expect(newClient.cache.get(scoped.cacheKey)).not.toBeDefined();
newClient.hydrate([dehydratedResponse]);
expect(newClient.cache.get(sk)).toStrictEqual(
expect.objectContaining({ ...response, cacheKey: scoped.cacheKey, staleTime: scoped.staleTime }),
);
});
it("should skip hydrate when override is false and scoped slot already has data", async () => {
const scoped = request.setScope("no-over").setMock(() => ({ data: "a", status: 200 }));
await scoped.send();
const ts = Date.now();
const dehydratedResponse = scoped.dehydrate({
response: {
data: "b",
status: 200,
success: true,
error: null,
extra: xhrExtra,
requestTimestamp: ts,
responseTimestamp: ts,
},
});
const newClient = new Client({ url: "shared-base-url" });
const existingReq = newClient
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setScope("no-over");
const sk = scopeKey(existingReq.cacheKey, "no-over");
newClient.cache.set(existingReq.setCache(true), {
data: "existing",
error: null,
status: 200,
success: true,
extra: xhrExtra,
retries: 0,
requestTimestamp: Date.now(),
responseTimestamp: Date.now(),
addedTimestamp: Date.now(),
triggerTimestamp: Date.now(),
isCanceled: false,
isOffline: false,
willRetry: false,
});
newClient.hydrate([dehydratedResponse], { override: false });
expect(newClient.cache.get(sk)?.data).toBe("existing");
});
});
});