Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active October 21, 2022 14:45
Show Gist options
  • Save baetheus/6733304ff22b7dbbf45e724491af432d to your computer and use it in GitHub Desktop.
Save baetheus/6733304ff22b7dbbf45e724491af432d to your computer and use it in GitHub Desktop.
New dux simplified interfaces
/**
* An action is the standard action that most
* applications deal with. It is typed, like TypedAction, but
* also has a payload associated with it.
*/
export type Action<P = void> = {
readonly tag: string;
readonly payload: P;
};
/**
* An action containing an any payload. This is
* useful for constraining a function input to any
* action.
*/
export type AnyAction = Action<unknown>;
/**
* Extract the type const from an Action
*/
export type ToPayload<A> = A extends Action<infer P> ? P : never;
/**
* An action matcher is a struct with a match function that takes
* an action and narrows it if its tag matches.
*/
export type ActionMatcher<P = void> = {
readonly tag: string;
readonly match: (action: AnyAction) => action is Action<P>;
};
/**
* An action function takes a payload and constructs an action from a payload.
* If the payload is void then the ActionFunction takes no arguments. An
* ActionCreator can be contramapped to change the inputs that create
* the action payload.
*/
export type ActionCreator<PS extends unknown[] = [], P = void> = P extends void
? () => Action<P>
: (...prepare: PS) => Action<P>;
/**
* An action factory is a function that creates an action by
* seeding the type and has a match property that acts as a
* refinement for any action. In other words it is an
* ActionFunction and an ActionMatcher.
*/
export type ActionFactory<PS extends unknown[] = [], P = void> =
& ActionCreator<PS, P>
& ActionMatcher<P>;
/**
* An ActionFactory type that will match any ActionFactory. This is
* useful for constraining an action factory as the input to a function.
*/
// deno-lint-ignore no-explicit-any
export type AnyActionFactory = ActionFactory<any[], any>;
/**
* Extract the payloads from a tuple of ActionFactories.
*/
export type ExtractPayloads<Factories> = Factories extends
ActionFactory<infer _, infer Payloads> ? Payloads : never;
/**
* Create an ActionFactory. This creates and merges an ActionCreator
* and an ActionMatcher
*/
export function createAction<P = void>(
tag: string,
): ActionFactory<[P], P> {
const fn = ((payload) => ({ tag, payload })) as ActionCreator<[P], P>;
const match: ActionMatcher<P> = {
tag,
match: (action): action is Action<P> => action.tag === tag,
};
return Object.assign(fn, match);
}
/**
* Prepare an action by changing the arguments used to
* create the action. This is an implementation of
* contramap on the ActionFactory type.
*/
export function contramap<AS extends unknown[], B>(fab: (...as: AS) => B) {
return (factory: ActionFactory<[B], B>): ActionFactory<AS, B> => {
const fn = ((...as: AS) => factory(fab(...as))) as ActionCreator<AS, B>;
const match = { match: factory.match, tag: factory.tag };
return Object.assign(fn, match);
};
}
/**
* A success payload represents the successful
* result of an effect.
*/
export type SuccessPayload<P = void, S = void> = {
readonly payload: P;
readonly success: S;
};
/**
* A failure payload represents the failed
* result of an effect.
*/
export type FailurePayload<P = void, F = void> = {
readonly payload: P;
readonly failure: F;
};
/**
* A cancelled payload represents the
* result of a cancelled effect
*/
export type CancelPayload<P = void, R = void> = {
readonly payload: P;
readonly reason: R;
};
/**
* The action string consts to add to tags in an
* effect group
*/
const INITIAL = "initial";
type INITIAL = typeof INITIAL;
const SUCCESS = "success";
type SUCCESS = typeof SUCCESS;
const FAILURE = "failure";
type FAILURE = typeof FAILURE;
const CANCEL = "cancel";
type CANCEL = typeof CANCEL;
/**
* A grouping of action creators representing the
* initiating, success, failure, and cancellation
* of an effect.
*/
export type EffectActionFactory<
P = void,
S = void,
F = void,
R = void,
> = {
initial: ActionFactory<[params: P], P>;
success: ActionFactory<[success: S, params: P], SuccessPayload<P, S>>;
failure: ActionFactory<[failure: F, params: P], FailurePayload<P, F>>;
cancel: ActionFactory<[reason: R, params: P], CancelPayload<P, R>>;
};
/**
* A package of action creators around an effect
* generally relating to asynchronous action
*/
export function createEffectActions<
P = void,
S = void,
F = void,
R = void,
>(tag: string): EffectActionFactory<P, S, F, R> {
return {
initial: createAction(`${tag}/${INITIAL}`),
success: contramap((success: S, payload: P) => ({ success, payload }))(
createAction(`${tag}/${SUCCESS}`),
),
failure: contramap((failure: F, payload: P) => ({ failure, payload }))(
createAction(`${tag}/${FAILURE}`),
),
cancel: contramap((reason: R, payload: P) => ({ reason, payload }))(
createAction(`${tag}/${CANCEL}`),
),
};
}
import type {
ActionFactory,
AnyAction,
AnyActionFactory,
ExtractPayloads,
} from "./action.ts";
import { pipe } from "https://deno.land/x/fun/fn.ts";
/**
* A reducer takes some state and some value and returns
* a new state. It is implied that state is immutable, but
* it is not necessary.
*/
export type Reducer<S, A> = (s: S, a: A) => S;
/**
* A metareducer takes in a reducer, modifies it some way, and
* returns the modified reducer. This modification can be
* inserting default state, logging the state before and
* after a reducer runs, wrapping the reducer in a try/catch
* block, or many other things.
*/
export type MetaReducer<S, A extends AnyAction> = (
reducer: Reducer<S, A>,
) => Reducer<S, A>;
/**
* Handle a single action by inferring the payload type from
* an action factory and extracting it for use in the passed
* reducer.
*/
export function caseFn<S, D extends unknown[], P>(
factory: ActionFactory<D, P>,
reducer: Reducer<S, P>,
): Reducer<S, AnyAction> {
return (s, a) => factory.match(a) ? reducer(s, a.payload) : s;
}
/**
* Handle multiple actions by inferring the payload type from
* action factories and extracting it for use in the passed
* reducer.
*/
export function casesFn<S, F extends AnyActionFactory[]>(
factories: F,
reducer: Reducer<S, ExtractPayloads<F>>,
): Reducer<S, AnyAction> {
return (s, a) =>
factories.some((factory) => factory.match(a))
? reducer(s, a.payload as ExtractPayloads<F>)
: s;
}
/**
* Combine multiple reducers into one reducer with an
* initial state
*/
export function reducerFn<S>(
initialState: S,
...reducers: Reducer<S, AnyAction>[]
): Reducer<S, AnyAction> {
return (initial = initialState, action) =>
reducers.reduce((state, reducer) => reducer(state, action), initial);
}
/**
* Combine multiple reducers into one Reducer
*/
export function reducersFn<S>(
...reducers: Reducer<S, AnyAction>[]
): Reducer<S, AnyAction> {
return (s, action) =>
reducers.reduce((state, reducer) => reducer(state, action), s);
}
/**
* Apply a reducer to a slice of larger state.
*/
export function at<R extends Record<string, unknown>, K extends keyof R>(
key: K,
reducer: Reducer<R[K], AnyAction>,
): Reducer<R, AnyAction> {
return (r, a) => {
const substate = reducer(r[key], a);
return substate === r[key] ? r : { ...r, [key]: substate };
};
}
import * as R from "../reducer.ts";
import * as A from "../action.ts";
// --- TODOS ---
// todoReducer has no concept of parent state
type Todo = {
value: string;
status: "Incomplete" | "Complete" | "Archived";
};
const todo = (value: string, status: Todo["status"] = "Incomplete"): Todo => ({
value,
status,
});
const createTodo = A.createAction<Todo>("Create Todo");
const completeTodo = A.createAction<Todo>("Complete Todo");
const archiveTodo = A.createAction<Todo>("Archive Todo");
const todoReducer = R.reducerFn<Todo[]>(
[],
R.caseFn(createTodo, (todos, todo) => todos.concat(todo)),
R.caseFn(
completeTodo,
(todos, t1) =>
todos.map((t2) => t1 === t2 ? todo(t1.value, "Complete") : t2),
),
R.caseFn(
archiveTodo,
(todos, t1) =>
todos.map((t2) => t1 === t2 ? todo(t1.value, "Archived") : t2),
),
);
// --- State ---
// Parent state knows where the type todoReducer expects
// and must make it a key
type State = {
todos: Todo[]; // We have todos
countActions: number; // Any some other state
};
const initialState: State = { todos: [], countActions: 0 };
export const stateReducer = R.reducerFn(
initialState,
R.at("todos", todoReducer), // Reduce at "todos" using the todoReducer
R.at("countActions", (s) => s + 1), // Reduce at countActions with this one
(s, _) => s, // If I wanted to use all state I can easily do so.
);
// -- Super State --
// Nesting level is arbitrary as long as its a Record<string, whatever>
type SuperState = {
state: State;
};
const initialSuperState: SuperState = { state: initialState };
export const superStateReducer = R.reducerFn(
initialSuperState,
R.at("state", stateReducer),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment