UNPKG

@equinor/fusion-observable

Version:
122 lines 5.57 kB
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