Middleware & epics

Wrap dispatch with middleware, or turn an RxJS event stream into new events. Keep cancellation and external effects under explicit ownership.

Wrap dispatch deliberately

applyMiddlewares(middlewares) is an enhancer. A Middleware<S> receives { state, dispatch }, then next, then an event. Call next(event) to continue. In the current implementation, middlewareApi.dispatch is the dispatch passed into this enhancer, rather than a reference to the final wrapped dispatch.

The array is reduced left to right: for [a, b], b wraps a, so b runs first on the way in. Enhancers themselves are applied in array order. Do not assume Redux’s middleware composition or recursive dispatch semantics.

A logging middleware
import { applyMiddlewares, configureStore, type Middleware } from "@carere/solux";

type State = { value: number };
const logger: Middleware<State> = (api) => (next) => (event) => {
  console.log("before", event.type, api.state.value);
  next(event);
  console.log("after", api.state.value);
};
// rootSlice is a Slice<State> created for this store.
const store = configureStore({
  rootSlice,
  enhancers: [applyMiddlewares([logger])],
});

Transform event streams with RxJS

An Epic<State, Container> receives an event Observable, readonly-typed live state, and an optional container. It returns an Observable of events. combineEpics(...epics) merges their output streams. epicMiddleware({ rootEpic, container? }) connects a root epic to dispatch; the input event reaches next before the epic sees it.

Epic outputs use that epic middleware’s dispatch function. They are passed downstream and fed back into its event stream, so filter inputs carefully to avoid a feedback loop. Readonly typing is not a runtime freeze or a detached state snapshot.

A small event transformation
import { applyMiddlewares, combineEpics, configureStore,
  createEvent, epicMiddleware, type Epic } from "@carere/solux";
import { filter, map } from "rxjs";

const requested = createEvent<number>("counter/requested");
const applied = createEvent<number>("counter/applied");
type State = { value: number };
const applyRequest: Epic<State> = (event$) => event$.pipe(
  filter((event): event is ReturnType<typeof requested> =>
    event.type === requested.type),
  map((event) => applied(event.payload)),
);
// rootSlice handles applied; create it fresh for this store.
const store = configureStore({
  rootSlice,
  enhancers: [applyMiddlewares([
    epicMiddleware({ rootEpic: combineEpics(applyRequest) }),
  ])],
});

Own the lifetime of async work

Manage cancellation explicitly with caller-owned abort controllers, RxJS takeUntil signals, and timer cleanup. Apply takeUntil at the root output stream to end the observable work you own, and signal it during scope cleanup. The current epic middleware does not expose a separate teardown API.

Handle errors inside your epic pipelines so an unhandled error does not terminate the intended flow. A time-travel state restoration does not undo network requests, rerun epics deterministically, or restore external resources.