Last active
October 21, 2022 14:45
-
-
Save baetheus/6733304ff22b7dbbf45e724491af432d to your computer and use it in GitHub Desktop.
New dux simplified interfaces
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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}`), | |
), | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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