@equinor/fusion-observable
Version:
122 lines • 5.57 kB
JavaScript
import { produce as createNextState, isDraft, isDraftable } from 'immer';
function freezeDraftable(val) {
// biome-ignore lint/suspicious/noEmptyBlockStatements: This is a valid use case for an empty block statement
return isDraftable(val) ? createNextState(val, () => { }) : val;
}
function isStateFunction(x) {
return typeof x === 'function';
}
export function createReducer(initialState, mapOrBuilderCallback) {
const [actionsMap, finalActionMatchers, finalDefaultCaseReducer] = executeReducerBuilderCallback(mapOrBuilderCallback);
// Ensure the initial state gets frozen either way (if draftable)
let getInitialState;
if (isStateFunction(initialState)) {
getInitialState = () => freezeDraftable(initialState());
}
else {
const frozenInitialState = freezeDraftable(initialState);
getInitialState = () => frozenInitialState;
}
function reducer(state, action) {
let caseReducers = [
actionsMap[action.type],
...finalActionMatchers.filter(({ matcher }) => matcher(action)).map(({ reducer }) => reducer),
];
if (finalDefaultCaseReducer && caseReducers.filter((cr) => !!cr).length === 0) {
caseReducers = [finalDefaultCaseReducer];
}
return caseReducers.reduce((previousState, caseReducer) => {
if (caseReducer) {
if (isDraft(previousState)) {
// If it's already a draft, we must already be inside a `createNextState` call,
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
// inside an existing draft. It's safe to just pass the draft to the mutator.
const draft = previousState; // We can assume this is already a draft
const result = caseReducer(draft, action);
if (result === undefined) {
return previousState;
}
return result;
}
else if (!isDraftable(previousState)) {
// If state is not draftable (ex: a primitive, such as 0), we want to directly
// return the caseReducer func and not wrap it with produce.
const result = caseReducer(previousState, action);
if (result === undefined) {
if (previousState === null) {
return previousState;
}
throw Error('A case reducer on a non-draftable value must not return undefined');
}
return result;
}
else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// createNextState() produces an Immutable<Draft<S>> rather
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
// these two types.
return createNextState(previousState, (draft) => {
return caseReducer(draft, action);
});
}
}
return previousState;
}, state ?? getInitialState());
}
reducer.getInitialState = getInitialState;
return reducer;
}
export function executeReducerBuilderCallback(builderCallback) {
const actionsMap = {};
const actionMatchers = [];
let defaultCaseReducer;
const builder = {
addCase(typeOrActionCreator, reducer) {
if (process.env.NODE_ENV !== 'production') {
/*
* to keep the definition by the user in line with actual behavior,
* we enforce `addCase` to always be called before calling `addMatcher`
* as matching cases take precedence over matchers
*/
if (actionMatchers.length > 0) {
throw new Error('`builder.addCase` should only be called before calling `builder.addMatcher`');
}
if (defaultCaseReducer) {
throw new Error('`builder.addCase` should only be called before calling `builder.addDefaultCase`');
}
}
const type = typeof typeOrActionCreator === 'string' ? typeOrActionCreator : typeOrActionCreator.type;
if (type in actionsMap) {
throw new Error('addCase cannot be called with two reducers for the same action type');
}
actionsMap[type] = reducer;
return builder;
},
addMatcher(matcher, reducer) {
if (process.env.NODE_ENV !== 'production') {
if (defaultCaseReducer) {
throw new Error('`builder.addMatcher` should only be called before calling `builder.addDefaultCase`');
}
}
actionMatchers.push({ matcher, reducer });
return builder;
},
addDefaultCase(reducer) {
if (process.env.NODE_ENV !== 'production') {
if (defaultCaseReducer) {
throw new Error('`builder.addDefaultCase` can only be called once');
}
}
defaultCaseReducer = reducer;
return builder;
},
};
builderCallback(builder);
return [
actionsMap,
actionMatchers,
defaultCaseReducer,
];
}
//# sourceMappingURL=create-reducer.js.map