Migrated selectServer action to RTK and moved loadMercureInfo to an action listener

This commit is contained in:
Alejandro Celaya 2022-11-10 22:44:25 +01:00
parent 2e0e24d87b
commit 6221f9ed05
6 changed files with 71 additions and 68 deletions

View file

@ -20,5 +20,5 @@ export const setUpStore = (container: IContainer) => configureStore({
preloadedState,
middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk(
{ immutableCheck: false, serializableCheck: false }, // State is too big for these
).concat(save(localStorageConfig)),
).prepend(container.selectServerListener.middleware).concat(save(localStorageConfig)),
});

View file

@ -1,5 +1,5 @@
import { IContainer } from 'bottlejs';
import { combineReducers } from 'redux';
import { combineReducers } from '@reduxjs/toolkit';
import { serversReducer } from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';

View file

@ -1,15 +1,13 @@
import { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createListenerMiddleware, PayloadAction } from '@reduxjs/toolkit';
import { identity, memoizeWith, pipe } from 'ramda';
import { Action, Dispatch } from 'redux';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
import { SelectedServer } from '../data';
import { GetState } from '../../container/types';
import { isReachableServer, SelectedServer } from '../data';
import { ShlinkHealth } from '../../api/types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { buildReducer, createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
export const SELECT_SERVER = 'shlink/selectedServer/selectServer';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/resetSelectedServer';
export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999';
@ -35,50 +33,49 @@ const initialState: SelectedServer = null;
export default buildReducer<SelectedServer, SelectServerAction>({
[RESET_SELECTED_SERVER]: () => initialState,
[SELECT_SERVER]: (_, { payload }) => payload,
[`${SELECT_SERVER}/fulfilled`]: (_, { payload }) => payload,
}, initialState);
export const resetSelectedServer = buildActionCreator(RESET_SELECTED_SERVER);
export const resetSelectedServer = createAction<void>(RESET_SELECTED_SERVER);
export const selectServer = (
buildShlinkApiClient: ShlinkApiClientBuilder,
loadMercureInfo: () => Action,
) => (
serverId: string,
) => async (
dispatch: Dispatch,
getState: GetState,
) => {
) => createAsyncThunk(SELECT_SERVER, async (serverId: string, { dispatch, getState }): Promise<SelectedServer> => {
dispatch(resetSelectedServer());
const { servers } = getState();
const selectedServer = servers[serverId];
if (!selectedServer) {
dispatch<SelectServerAction>({
type: SELECT_SERVER,
payload: { serverNotFound: true },
});
return;
return { serverNotFound: true };
}
try {
const { health } = buildShlinkApiClient(selectedServer);
const { version, printableVersion } = await getServerVersion(serverId, health);
dispatch<SelectServerAction>({
type: SELECT_SERVER,
payload: {
...selectedServer,
version,
printableVersion,
},
});
dispatch(loadMercureInfo());
return {
...selectedServer,
version,
printableVersion,
};
} catch (e) {
dispatch<SelectServerAction>({
type: SELECT_SERVER,
payload: { ...selectedServer, serverNotReachable: true },
});
return { ...selectedServer, serverNotReachable: true };
}
});
export const selectServerListener = (
selectServerThunk: ReturnType<typeof selectServer>,
loadMercureInfo: () => PayloadAction<any>, // TODO Consider setting actual type, if relevant
) => {
const listener = createListenerMiddleware();
listener.startListening({
actionCreator: selectServerThunk.fulfilled,
effect: ({ payload }, { dispatch }) => {
isReachableServer(payload) && dispatch(loadMercureInfo());
},
});
return listener;
};

View file

@ -5,7 +5,7 @@ import { DeleteServerModal } from '../DeleteServerModal';
import { DeleteServerButton } from '../DeleteServerButton';
import { EditServer } from '../EditServer';
import { ImportServersBtn } from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
import { resetSelectedServer, selectServer, selectServerListener } from '../reducers/selectedServer';
import { createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers';
import { fetchServers } from '../reducers/remoteServers';
import { ServerError } from '../helpers/ServerError';
@ -77,6 +77,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('fetchServers', fetchServers, 'axios');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
// Reducers
bottle.serviceFactory('selectServerListener', selectServerListener, 'selectServer', 'loadMercureInfo');
};
export default provideServices;

View file

@ -1,13 +1,11 @@
import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { CreateVisit } from '../types';
const CREATE_VISITS = 'shlink/visitCreation/CREATE_VISITS';
export type CreateVisitsAction = PayloadAction<{
createdVisits: CreateVisit[];
}>;
export const createNewVisits = createAction(
CREATE_VISITS,
'shlink/visitCreation/createNewVisits',
(createdVisits: CreateVisit[]) => ({ payload: { createdVisits } }),
);

View file

@ -1,31 +1,36 @@
import { v4 as uuid } from 'uuid';
import { Mock } from 'ts-mockery';
import reducer, {
selectServer,
selectServer as selectServerCreator,
resetSelectedServer,
RESET_SELECTED_SERVER,
SELECT_SERVER,
MAX_FALLBACK_VERSION,
MIN_FALLBACK_VERSION,
} from '../../../src/servers/reducers/selectedServer';
import { ShlinkState } from '../../../src/container/types';
import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
describe('selectedServerReducer', () => {
const health = jest.fn();
const buildApiClient = jest.fn().mockReturnValue(Mock.of<ShlinkApiClient>({ health }));
const selectServer = selectServerCreator(buildApiClient);
afterEach(jest.clearAllMocks);
describe('reducer', () => {
it('returns default when action is RESET_SELECTED_SERVER', () =>
expect(reducer(null, { type: RESET_SELECTED_SERVER, payload: null })).toBeNull());
expect(reducer(null, { type: resetSelectedServer.toString(), payload: null })).toBeNull());
it('returns selected server when action is SELECT_SERVER', () => {
const payload = Mock.of<RegularServer>({ id: 'abc123' });
expect(reducer(null, { type: SELECT_SERVER, payload })).toEqual(payload);
expect(reducer(null, { type: selectServer.fulfilled.toString(), payload })).toEqual(payload);
});
});
describe('resetSelectedServer', () => {
it('returns proper action', () => {
expect(resetSelectedServer()).toEqual({ type: RESET_SELECTED_SERVER });
expect(resetSelectedServer()).toEqual({ type: resetSelectedServer.toString() });
});
});
@ -35,14 +40,7 @@ describe('selectedServerReducer', () => {
};
const version = '1.19.0';
const createGetStateMock = (id: string) => jest.fn().mockReturnValue({ servers: { [id]: selectedServer } });
const apiClientMock = {
health: jest.fn(),
};
const buildApiClient = jest.fn().mockReturnValue(apiClientMock);
const dispatch = jest.fn();
const loadMercureInfo = jest.fn();
afterEach(jest.clearAllMocks);
it.each([
[version, version, `v${version}`],
@ -57,21 +55,24 @@ describe('selectedServerReducer', () => {
printableVersion: expectedPrintableVersion,
};
apiClientMock.health.mockResolvedValue({ version: serverVersion });
health.mockResolvedValue({ version: serverVersion });
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
await selectServer(id)(dispatch, getState, {});
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, payload: expectedSelectedServer });
expect(loadMercureInfo).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: selectServer.pending.toString() }));
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: resetSelectedServer.toString() }));
expect(dispatch).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: selectServer.fulfilled.toString(),
payload: expectedSelectedServer,
}));
});
it('invokes dependencies', async () => {
const id = uuid();
const getState = createGetStateMock(id);
await selectServer(buildApiClient, loadMercureInfo)(id)(jest.fn(), getState);
await selectServer(id)(jest.fn(), getState, {});
expect(getState).toHaveBeenCalledTimes(1);
expect(buildApiClient).toHaveBeenCalledTimes(1);
@ -82,13 +83,15 @@ describe('selectedServerReducer', () => {
const getState = createGetStateMock(id);
const expectedSelectedServer = Mock.of<NonReachableServer>({ ...selectedServer, serverNotReachable: true });
apiClientMock.health.mockRejectedValue({});
health.mockRejectedValue({});
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
await selectServer(id)(dispatch, getState, {});
expect(apiClientMock.health).toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, payload: expectedSelectedServer });
expect(loadMercureInfo).not.toHaveBeenCalled();
expect(health).toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: selectServer.fulfilled.toString(),
payload: expectedSelectedServer,
}));
});
it('dispatches error when server is not found', async () => {
@ -96,12 +99,14 @@ describe('selectedServerReducer', () => {
const getState = jest.fn(() => Mock.of<ShlinkState>({ servers: {} }));
const expectedSelectedServer: NotFoundServer = { serverNotFound: true };
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
await selectServer(id)(dispatch, getState, {});
expect(getState).toHaveBeenCalled();
expect(apiClientMock.health).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, payload: expectedSelectedServer });
expect(loadMercureInfo).not.toHaveBeenCalled();
expect(health).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: selectServer.fulfilled.toString(),
payload: expectedSelectedServer,
}));
});
});
});