@data-client/core
Version:
Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch
261 lines (227 loc) • 7.04 kB
text/typescript
import type { EntityPath } from '@data-client/normalizr';
import { jest } from '@jest/globals';
import { has } from 'benchmark';
import { GC } from '../../actionTypes';
import Controller from '../../controller/Controller';
import { GCPolicy } from '../GCPolicy';
describe('GCPolicy', () => {
let gcPolicy: GCPolicy;
let controller: Controller;
beforeEach(() => {
controller = {
getState: jest.fn(),
dispatch: jest.fn(),
} as unknown as Controller;
gcPolicy = new GCPolicy();
gcPolicy.init(controller);
});
afterEach(() => {
gcPolicy.cleanup();
});
it('should increment and decrement endpoint and entity counts', () => {
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
expect(gcPolicy['endpointCount'].get(key)).toBe(1);
expect(gcPolicy['entityCount'].get('testEntity')?.get('1')).toBe(1);
decrement();
expect(gcPolicy['endpointCount'].get(key)).toBeUndefined();
expect(gcPolicy['entityCount'].get('testEntity')?.get('1')).toBeUndefined();
});
it('should dispatch GC action once no ref counts and is expired', () => {
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const state = {
meta: {
testEndpoint: {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
entitiesMeta: {
testEntity: {
'1': {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
},
};
(controller.getState as jest.Mock).mockReturnValue(state);
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
countRef(); // Increment again
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement();
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement(); // Decrement twice
gcPolicy['runSweep']();
expect(controller.dispatch).toHaveBeenCalledWith({
type: GC,
entities: [{ key: 'testEntity', pk: '1' }],
endpoints: ['testEndpoint'],
});
});
it('should dispatch GC action once no ref counts and is expired with extra decrements', () => {
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const state = {
meta: {
testEndpoint: {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
entitiesMeta: {
testEntity: {
'1': {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
},
};
(controller.getState as jest.Mock).mockReturnValue(state);
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
countRef(); // Increment again
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement();
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement(); // Decrement twice
decrement(); // Decrement extra time
gcPolicy['runSweep']();
expect(controller.dispatch).toHaveBeenCalledWith({
type: GC,
entities: [{ key: 'testEntity', pk: '1' }],
endpoints: ['testEndpoint'],
});
});
it('should dispatch GC action once no ref counts and no expiry state', () => {
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const state = {
meta: {},
entitiesMeta: {},
};
(controller.getState as jest.Mock).mockReturnValue(state);
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
countRef(); // Increment again
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement();
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
decrement(); // Decrement twice
gcPolicy['runSweep']();
expect(controller.dispatch).toHaveBeenCalledWith({
type: GC,
entities: [{ key: 'testEntity', pk: '1' }],
endpoints: ['testEndpoint'],
});
});
it('should not dispatch GC action if expiresAt has not passed, but dispatch later when it has', () => {
jest.useFakeTimers();
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const futureTime = Date.now() + 1000;
const state = {
meta: {
testEndpoint: {
date: futureTime - 100,
fetchAt: futureTime - 100,
expiresAt: futureTime,
},
},
entitiesMeta: {
testEntity: {
'1': {
date: futureTime - 100,
fetchAt: futureTime - 100,
expiresAt: futureTime,
},
},
},
};
(controller.getState as jest.Mock).mockReturnValue(state);
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
countRef(); // Increment again
decrement();
decrement(); // Decrement twice
gcPolicy['runSweep']();
expect(controller.dispatch).not.toHaveBeenCalled();
// Fast forward time to past the futureTime
jest.advanceTimersByTime(2000);
(controller.getState as jest.Mock).mockReturnValue({
meta: {
testEndpoint: {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
entitiesMeta: {
testEntity: {
'1': {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
},
});
gcPolicy['runSweep']();
expect(controller.dispatch).toHaveBeenCalledWith({
type: GC,
entities: [{ key: 'testEntity', pk: '1' }],
endpoints: ['testEndpoint'],
});
jest.useRealTimers();
});
it('should support custom hasExpired', () => {
jest.useFakeTimers();
gcPolicy = new GCPolicy({ expiresAt: () => 0 });
gcPolicy.init(controller);
const key = 'testEndpoint';
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
const futureTime = Date.now() + 1000;
const state = {
meta: {
testEndpoint: {
date: futureTime - 100,
fetchAt: futureTime - 100,
expiresAt: futureTime,
},
},
entitiesMeta: {
testEntity: {
'1': {
date: futureTime - 100,
fetchAt: futureTime - 100,
expiresAt: futureTime,
},
},
},
};
(controller.getState as jest.Mock).mockReturnValue(state);
const countRef = gcPolicy.createCountRef({ key, paths });
const decrement = countRef();
countRef(); // Increment again
decrement();
decrement(); // Decrement twice
gcPolicy['runSweep']();
expect(controller.dispatch).toHaveBeenCalled();
jest.useRealTimers();
});
});