pl-fe: Replace immer with mutative

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-05 20:45:57 +01:00
parent 7ca17884f8
commit cc858d4f0f
10 changed files with 133 additions and 108 deletions

View file

@ -87,7 +87,6 @@
"fuzzysort": "^3.0.2",
"graphemesplit": "^2.4.4",
"html-react-parser": "^5.1.16",
"immer": "^10.1.1",
"immutable": "^4.3.7",
"intersection-observer": "^0.12.2",
"intl-messageformat": "^10.5.14",
@ -100,6 +99,7 @@
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.9.1",
"multiselect-react-dropdown": "^2.0.25",
"mutative": "^1.0.11",
"path-browserify": "^1.0.1",
"pl-api": "^0.1.9",
"postcss": "^8.4.47",
@ -142,7 +142,8 @@
"vite-plugin-html": "^3.2.2",
"vite-plugin-require": "^1.2.14",
"vite-plugin-static-copy": "^1.0.6",
"zustand": "^5.0.0-rc.2"
"zustand": "^5.0.0-rc.2",
"zustand-mutative": "^1.0.5"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",

View file

@ -1,4 +1,4 @@
import { produce, enableMapSet } from 'immer';
import { create, type Immutable } from 'mutative';
import {
ENTITIES_IMPORT,
@ -17,12 +17,10 @@ import { createCache, createList, updateStore, updateList } from './utils';
import type { DeleteEntitiesOpts } from './actions';
import type { EntitiesTransaction, Entity, EntityCache, EntityListState, ImportPosition } from './types';
enableMapSet();
/** Entity reducer state. */
interface State {
type State = Immutable<{
[entityType: string]: EntityCache | undefined;
}
}>;
/** Import entities into the cache. */
const importEntities = (
@ -33,7 +31,7 @@ const importEntities = (
pos?: ImportPosition,
newState?: EntityListState,
overwrite = false,
): State => produce(state, draft => {
): State => create(state, draft => {
const cache = draft[entityType] ?? createCache();
cache.store = updateStore(cache.store, entities);
@ -54,14 +52,16 @@ const importEntities = (
}
draft[entityType] = cache;
});
return draft;
}, { enableAutoFreeze: true });
const deleteEntities = (
state: State,
entityType: string,
ids: Iterable<string>,
opts: DeleteEntitiesOpts,
) => produce(state, draft => {
) => create(state, draft => {
const cache = draft[entityType] ?? createCache();
for (const id of ids) {
@ -88,7 +88,7 @@ const dismissEntities = (
entityType: string,
ids: Iterable<string>,
listKey: string,
) => produce(state, draft => {
) => create(state, draft => {
const cache = draft[entityType] ?? createCache();
const list = cache.lists[listKey];
@ -110,7 +110,7 @@ const incrementEntities = (
entityType: string,
listKey: string,
diff: number,
) => produce(state, draft => {
) => create(state, draft => {
const cache = draft[entityType] ?? createCache();
const list = cache.lists[listKey];
@ -126,7 +126,7 @@ const setFetching = (
listKey: string | undefined,
isFetching: boolean,
error?: any,
) => produce(state, draft => {
) => create(state, draft => {
const cache = draft[entityType] ?? createCache();
if (typeof listKey === 'string') {
@ -139,13 +139,13 @@ const setFetching = (
draft[entityType] = cache;
});
const invalidateEntityList = (state: State, entityType: string, listKey: string) => produce(state, draft => {
const invalidateEntityList = (state: State, entityType: string, listKey: string) => create(state, draft => {
const cache = draft[entityType] ?? createCache();
const list = cache.lists[listKey] ?? createList();
list.state.invalid = true;
});
const doTransaction = (state: State, transaction: EntitiesTransaction) => produce(state, draft => {
const doTransaction = (state: State, transaction: EntitiesTransaction) => create(state, draft => {
for (const [entityType, changes] of Object.entries(transaction)) {
const cache = draft[entityType] ?? createCache();
for (const [id, change] of Object.entries(changes)) {

View file

@ -39,9 +39,9 @@ const UserIndex: React.FC = () => {
updateQuery();
}, []);
const hasMore = items.count() < total && !!next;
const hasMore = items.length < total && !!next;
const showLoading = isLoading && items.isEmpty();
const showLoading = isLoading && !items.length;
return (
<Column label={intl.formatMessage(messages.heading)}>

View file

@ -2,8 +2,7 @@
* Accounts Meta: private user data only the owner should see.
* @module pl-fe/reducers/accounts_meta
*/
import { produce } from 'immer';
import { create, type Immutable } from 'mutative';
import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS, type AuthAction } from 'pl-fe/actions/auth';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from 'pl-fe/actions/me';
@ -15,17 +14,17 @@ interface AccountMeta {
source: Account['__meta']['source'];
}
type State = Record<string, AccountMeta | undefined>;
type State = Immutable<Record<string, AccountMeta | undefined>>;
const importAccount = (state: State, account: CredentialAccount): State =>
produce(state, draft => {
create(state, draft => {
const existing = draft[account.id];
draft[account.id] = {
pleroma: account.__meta.pleroma ?? existing?.pleroma,
source: account.__meta.source ?? existing?.source,
};
});
}, { enableAutoFreeze: true });
const accounts_meta = (state: Readonly<State> = {}, action: AuthAction | MeAction): State => {
switch (action.type) {

View file

@ -1,4 +1,4 @@
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import { create } from 'mutative';
import {
ADMIN_USER_INDEX_EXPAND_FAIL,
@ -10,58 +10,74 @@ import {
ADMIN_USER_INDEX_QUERY_SET,
} from 'pl-fe/actions/admin';
import type { AnyAction } from '@reduxjs/toolkit';
import type { AdminAccount, AdminGetAccountsParams, PaginatedResponse } from 'pl-api';
import type { APIEntity } from 'pl-fe/types/entities';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
type State = {
isLoading: boolean;
loaded: boolean;
items: Array<string>;
total: number;
page: number;
query: string;
next: (() => Promise<PaginatedResponse<AdminAccount>>) | null;
params: AdminGetAccountsParams | null;
}
const initialState: State = {
isLoading: false,
loaded: false,
items: ImmutableOrderedSet<string>(),
items: [],
total: Infinity,
page: -1,
query: '',
next: null as (() => Promise<PaginatedResponse<AdminAccount>>) | null,
params: null as AdminGetAccountsParams | null,
});
next: null,
params: null,
};
type State = ReturnType<typeof ReducerRecord>;
const admin_user_index = (state: State = ReducerRecord(), action: AnyAction): State => {
const admin_user_index = (state: State = initialState, action: AnyAction): State => {
switch (action.type) {
case ADMIN_USER_INDEX_QUERY_SET:
return state.set('query', action.query);
return create(state, draft => {
draft.query = action.query;
});
case ADMIN_USER_INDEX_FETCH_REQUEST:
return state
.set('isLoading', true)
.set('loaded', true)
.set('items', ImmutableOrderedSet())
.set('total', action.total)
.set('page', 0)
.set('next', null);
return create(state, draft => {
draft.isLoading = true;
draft.loaded = true;
draft.items = [];
draft.total = action.total;
draft.page = 0;
draft.next = null;
});
case ADMIN_USER_INDEX_FETCH_SUCCESS:
return state
.set('isLoading', false)
.set('loaded', true)
.set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id)))
.set('total', action.total)
.set('page', 1)
.set('next', action.next);
return create(state, draft => {
draft.isLoading = false;
draft.loaded = true;
draft.items = action.users.map((user: APIEntity) => user.id);
draft.total = action.total;
draft.page = 1;
draft.next = action.next;
});
case ADMIN_USER_INDEX_FETCH_FAIL:
case ADMIN_USER_INDEX_EXPAND_FAIL:
return state
.set('isLoading', false);
return create(state, draft => {
draft.isLoading = false;
});
case ADMIN_USER_INDEX_EXPAND_REQUEST:
return state
.set('isLoading', true);
return create(state, draft => {
draft.isLoading = true;
});
case ADMIN_USER_INDEX_EXPAND_SUCCESS:
return state
.set('isLoading', false)
.set('loaded', true)
.set('items', state.items.union(action.users.map((user: APIEntity) => user.id)))
.set('total', action.total)
.set('page', 1)
.set('next', action.next);
return create(state, draft => {
draft.isLoading = false;
draft.loaded = true;
draft.items = [...new Set(draft.items.concat(action.users.map((user: APIEntity) => user.id)))];
draft.total = action.total;
draft.page = 1;
draft.next = action.next;
});
default:
return state;
}

View file

@ -1,5 +1,5 @@
import { produce } from 'immer';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { create } from 'mutative';
import { type Instance, instanceSchema } from 'pl-api';
import * as v from 'valibot';
@ -11,9 +11,11 @@ import ConfigDB from 'pl-fe/utils/config-db';
import type { AnyAction } from 'redux';
const initialState: Instance = v.parse(instanceSchema, {});
const initialState: State = v.parse(instanceSchema, {});
const preloadImport = (state: Instance, action: Record<string, any>, path: string) => {
type State = Instance;
const preloadImport = (state: State, action: Record<string, any>, path: string) => {
const instance = action.data[path];
return instance ? v.parse(instanceSchema, instance) : state;
};
@ -25,32 +27,30 @@ const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string)
return v ? v.getIn(['tuple', 1]) : undefined;
};
const importConfigs = (state: Instance, configs: ImmutableList<any>) => {
const importConfigs = (state: State, configs: ImmutableList<any>) => {
// FIXME: This is pretty hacked together. Need to make a cleaner map.
const config = ConfigDB.find(configs, ':pleroma', ':instance');
const simplePolicy = ConfigDB.toSimplePolicy(configs);
if (!config && !simplePolicy) return state;
return produce(state, (draft) => {
if (config) {
const value = config.get('value', ImmutableList());
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
if (config) {
const value = config.get('value', ImmutableList());
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
draft.registrations = {
enabled: registrationsOpen ?? draft.registrations.enabled,
approval_required: approvalRequired ?? draft.registrations.approval_required,
};
}
state.registrations = {
enabled: registrationsOpen ?? state.registrations.enabled,
approval_required: approvalRequired ?? state.registrations.approval_required,
};
}
if (simplePolicy) {
draft.pleroma.metadata.federation.mrf_simple = simplePolicy;
}
});
if (simplePolicy) {
state.pleroma.metadata.federation.mrf_simple = simplePolicy;
}
};
const handleAuthFetch = (state: Instance) => {
const handleAuthFetch = (state: State) => {
// Authenticated fetch is enabled, so make the instance appear censored
return {
...state,
@ -78,7 +78,7 @@ const persistInstance = (instance: { domain: string }, host: string | null = get
}
};
const handleInstanceFetchFail = (state: Instance, error: Record<string, any>) => {
const handleInstanceFetchFail = (state: State, error: Record<string, any>) => {
if (error.response?.status === 401) {
return handleAuthFetch(state);
} else {
@ -86,10 +86,10 @@ const handleInstanceFetchFail = (state: Instance, error: Record<string, any>) =>
}
};
const instance = (state = initialState, action: AnyAction | InstanceAction | PreloadAction): Instance => {
const instance = (state = initialState, action: AnyAction | InstanceAction | PreloadAction): State => {
switch (action.type) {
case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action, '/api/v1/instance');
return create(state, (draft) => preloadImport(draft, action, '/api/v1/instance'));
case INSTANCE_FETCH_SUCCESS:
persistInstance(action.instance);
return action.instance;
@ -97,10 +97,9 @@ const instance = (state = initialState, action: AnyAction | InstanceAction | Pre
return handleInstanceFetchFail(state, action.error);
case ADMIN_CONFIG_UPDATE_REQUEST:
case ADMIN_CONFIG_UPDATE_SUCCESS:
return importConfigs(state, ImmutableList(fromJS(action.configs)));
return create(state, (draft) => importConfigs(draft, ImmutableList(fromJS(action.configs))));
default:
return state;
}
};
export { instance as default };

View file

@ -1,12 +1,10 @@
import { configureStore, Tuple } from '@reduxjs/toolkit';
import { configureStore, Tuple, type AnyAction } from '@reduxjs/toolkit';
import { thunk, type ThunkDispatch } from 'redux-thunk';
import errorsMiddleware from './middleware/errors';
import soundsMiddleware from './middleware/sounds';
import appReducer from './reducers';
import type { AnyAction } from 'redux';
const store = configureStore({
reducer: appReducer,
middleware: () => new Tuple(
@ -17,6 +15,8 @@ const store = configureStore({
devTools: true,
});
(window as any).store = store;
type Store = typeof store;
// Infer the `RootState` and `AppDispatch` types from the store itself

View file

@ -1,5 +1,5 @@
import { produce } from 'immer';
import { create } from 'zustand';
import { mutative } from 'zustand-mutative';
import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address';
import type { ModalType } from 'pl-fe/features/ui/components/modal-root';
@ -86,12 +86,12 @@ type State = {
closeModal: (modalType?: ModalType) => void;
};
const useModalsStore = create<State>((set) => ({
const useModalsStore = create<State>()(mutative((set) => ({
modals: [],
openModal: (...[modalType, modalProps]) => set(produce((state: State) => {
openModal: (...[modalType, modalProps]) => set((state: State) => {
state.modals.push({ modalType, modalProps });
})),
closeModal: (modalType) => set(produce((state: State) => {
}),
closeModal: (modalType) => set((state: State) => {
if (state.modals.length === 0) {
return;
}
@ -100,7 +100,7 @@ const useModalsStore = create<State>((set) => ({
} else if (state.modals.some((modal) => modalType === modal.modalType)) {
state.modals = state.modals.slice(0, state.modals.findLastIndex((modal) => modalType === modal.modalType));
}
})),
}));
}),
}), { enableAutoFreeze: true }));
export { useModalsStore };

View file

@ -1,6 +1,6 @@
import { produce } from 'immer';
import * as v from 'valibot';
import { create } from 'zustand';
import { mutative } from 'zustand-mutative';
import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings';
@ -35,39 +35,39 @@ const changeSetting = (object: APIEntity, path: string[], value: any) => {
const mergeSettings = (state: State) => state.settings = { ...state.defaultSettings, ...state.userSettings };
const useSettingsStore = create<State>((set) => ({
const useSettingsStore = create<State>()(mutative((set) => ({
defaultSettings: v.parse(settingsSchema, {}),
userSettings: {},
settings: v.parse(settingsSchema, {}),
loadDefaultSettings: (settings: APIEntity) => set(produce((state: State) => {
loadDefaultSettings: (settings: APIEntity) => set((state: State) => {
if (typeof settings !== 'object') return;
state.defaultSettings = v.parse(settingsSchema, settings);
mergeSettings(state);
})),
}),
loadUserSettings: (settings?: APIEntity) => set(produce((state: State) => {
loadUserSettings: (settings?: APIEntity) => set((state: State) => {
if (typeof settings !== 'object') return;
state.userSettings = v.parse(settingsSchemaPartial, settings);
mergeSettings(state);
})),
}),
userSettingsSaving: () => set(produce((state: State) => {
userSettingsSaving: () => set((state: State) => {
state.userSettings.saved = true;
mergeSettings(state);
})),
}),
changeSetting: (path: string[], value: any) => set(produce((state: State) => {
changeSetting: (path: string[], value: any) => set((state: State) => {
changeSetting(state.userSettings, path, value);
mergeSettings(state);
})),
}),
rememberEmojiUse: (emoji: Emoji) => set(produce((state: State) => {
rememberEmojiUse: (emoji: Emoji) => set((state: State) => {
const settings = state.userSettings;
if (!settings.frequentlyUsedEmojis) settings.frequentlyUsedEmojis = {};
@ -75,9 +75,9 @@ const useSettingsStore = create<State>((set) => ({
settings.saved = false;
mergeSettings(state);
})),
}),
rememberLanguageUse: (language: string) => set(produce((state: State) => {
rememberLanguageUse: (language: string) => set((state: State) => {
const settings = state.userSettings;
if (!settings.frequentlyUsedLanguages) settings.frequentlyUsedLanguages = {};
@ -85,8 +85,8 @@ const useSettingsStore = create<State>((set) => ({
settings.saved = false;
mergeSettings(state);
})),
}));
}),
}), { enableAutoFreeze: true }));
export { useSettingsStore };

View file

@ -5812,7 +5812,7 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immer@^10.0.3, immer@^10.1.1:
immer@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
@ -7146,6 +7146,11 @@ multiselect-react-dropdown@^2.0.25:
resolved "https://registry.yarnpkg.com/multiselect-react-dropdown/-/multiselect-react-dropdown-2.0.25.tgz#0c8d16f20d78023d5be2f3af4f15a4a164b6b427"
integrity sha512-z8kUSyBNOuV7vn4Dk35snzXWtIfTdSEEXhgDdLMvOmR+xJFx35vc1voUlSuOvrk3khb+hXC219Qs9szOvNm25Q==
mutative@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/mutative/-/mutative-1.0.11.tgz#a03b48151800129fbc464257eff9ffa869c648a8"
integrity sha512-DfxsNvHfJlxp5yul7jhcNSI0EEWlP1vatiOr6Q7cvr8RNFBbIU5nENilUULbNJiOtbXznOxgbxHf4cYbqPDPlg==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
@ -10429,6 +10434,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zustand-mutative@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zustand-mutative/-/zustand-mutative-1.0.5.tgz#00ce93900a25d812b48d3f0d360da8a3890d0dd9"
integrity sha512-/Rc2huxi2SxD21EcSrwW6moCRv+phFcrv3bMyrsQMDkI49LqLqMzhfNCZnOv0TmUctgG0u9xD6gHLUoYEwjkkQ==
zustand@^5.0.0-rc.2:
version "5.0.0-rc.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0-rc.2.tgz#d28d95ffb6f0b20cadbaea39210f18446a5bf989"