import { Unsubscribe, createStore } from "redux" /** * The only thing all action types have in common is a string * property called type, which must be unique across all actions * handled by the same reducer. * * We adopt the additional (popular) pattern of collecting any * further data into a single payload property. This makes the * very common case of a single value very succinct. */ export interface Action { readonly type: T; readonly payload: P; } /** * Common features of an ActionCreator and a CollectionDefinition. */ export interface ActionDefinition { readonly type: T; reduce(state: S, payload: P): S; } export function definition( type: T, reduce: (state: S, payload: P) => S, func: F ): F & ActionDefinition { return assign(func, { type, reduce }); } /** * An ActionCreator is a function that creates actions and can * also be registered with a reducer. */ export interface ActionCreator extends ActionDefinition { (payload: P): Action; } /** * Defines an action, for later inclusion in a reducer. */ export function action( type: T, reduce: (state: S, payload: P) => S ): ActionCreator { function create(payload: P) { return { type, payload }; } return definition(type, reduce, create); } /** * Almost a minimal reducer, except it also knows a good * initial state, known as 'empty'. */ export interface Reducer { /** * Reduce function */ (state: S, action: A): S; /** * A suitable initial state */ empty: S; } /** * A reducer is a function that takes a state object and an action * and returns a modified state object. Here it is also equipped * with a method called action which allows multiple reducer * functions to be declaratively merged together into a single * function, and a store method that wraps Redux's createStore * to make it perfectly type-safe. * * Note that reducers are immutable - given a reducer x, calling * x.action(...) returns a new reducer combination rather than * modifying x. */ export interface ReducerBuilder extends Reducer { /** * Reduce function */ (state: S, action: A): S; /** * A suitable initial state */ empty: S; /** * Dummy member for use with typeof (does not have * a useful runtime value.) */ actionType: A; cursorType: Cursor; /** * Returns an enhanced ReducerBuilder capable of reducing * some additional action type. */ action( definition: ActionDefinition ): ReducerBuilder>; /** * Creates a Redux store with extra type-safety. */ store(): Store; mixin( reducer2: Reducer ): ReducerBuilder; } function isAction( obj: any, type: T ): obj is Action { return obj && obj.type === type; } function mixin( reducer1: Reducer, reducer2: Reducer ): ReducerBuilder { const empty = amend(reducer1.empty, reducer2.empty); function reduce(state: S1 & S2, action: A1 | A2) { if (state === undefined) { state = empty; } const result2 = reducer2(state, action as A2); if (result2 !== state) { return amend(state, result2); } const result1 = reducer1(state, action as A1); if (result1 !== state) { return amend(state, result1); } return state; } return builder(empty, reduce); } export function builder( empty: S, reduce: (state: S, action: A) => S ): ReducerBuilder { const result = assign(reduce, { actionType: undefined! as A, cursorType: undefined! as Cursor, empty, action( def: ActionDefinition ) : ReducerBuilder | A> { return chain(def.type, empty, def.reduce, reduce); }, store(): Store { return createStore(reduce); }, mixin(reducer2: Reducer): ReducerBuilder { return mixin(result, reducer2); } }); return result; } function chain( headType: HT, empty: S, head: (state: S, payload: HP) => S, rest: (state: S, action: RA) => S ): ReducerBuilder> { type A = RA | Action; function reduce(state: S, action: A) { if (isAction(action, headType)) { return head(state, action.payload); } return rest(state, action); } return builder(empty, reduce); } /** * Creates a starter object from which a ReducerBuilder can be formed by * calling the action method. */ export function reducer(empty: S) { return builder(empty, s => s === undefined ? empty : s); } export interface Subscribable { subscribe(listener: () => void): Unsubscribe; } /** * Describes a minimal Redux-like store. Note that stores are not * immutable (that's their whole purpose) and therefore getState is * not pure (it may return a different value each time you call it). */ export interface Store extends Subscribable { dispatch(action: A1): A1; getState(): S; } /** * A pure representation of the state of a store or part of a store. * A cursor's value property never changes. Instead, the dispatch * method returns a new cursor representing the new state. * * Note that, unlike a traditional non-Redux cursor, updating is * always performed by dispatching an action. */ export interface Cursor { /** * The state at the time this cursor was created. */ readonly state: S; /** * Sends an action into the store's reducer, resulting in the * store updating, and a new cursor is returned representing * the new state. */ (action: A): Cursor; /** * Piping operator - allows left-to-write composition to navigate * down through a tree of cursors. This variant is for simple * references or properties. */ $(ref: (outer: Cursor) => Cursor): Cursor; } export function cursor(state: S, dispatch: (action: A) => Cursor): Cursor { let outer: Cursor; function pipe(ref: (outer: Cursor) => Cursor): Cursor { return ref(outer); } outer = assign(dispatch, { $: pipe, state, valueOf() { return state; } }); return outer; } /** * Takes a snapshot of a Redux-like store, making it into a pure cursor. */ export function snapshot(store: Store): Cursor { return cursor(store.getState(), (action: A) => { store.dispatch(action); return snapshot(store); }); } export interface ReferenceDefinition extends ActionDefinition { (outer: Cursor>): Cursor; update(action: A): Action; } function item( fetch: (outer: OS) => IS, update: (action: IA) => OA ) { return (outer: Cursor): Cursor => { return cursor(fetch(outer.state), (innerAction: IA) => item(fetch, update)(outer(update(innerAction)))); }; } export interface ReducerProvider { reduce: Reducer; } export type ReducerOrProvider = Reducer | ReducerProvider; function isReducerProvider(obj: any): obj is ReducerProvider { return typeof obj !== "function" && typeof obj.reduce === "function"; } export function getReducer(reducerOrProvider: ReducerOrProvider) { return isReducerProvider(reducerOrProvider) ? reducerOrProvider.reduce : reducerOrProvider; } export function reference( type: T, reducerOrProvider: ReducerOrProvider, get: (state: S) => I, set?: (state: S, item: I) => S ): ReferenceDefinition { const reducer = getReducer(reducerOrProvider); const ensuredSet = ensureReducer(`reference(${type})`, get, set); function update(payload: A): Action { return { type, payload }; } function reduce(outerState: S, innerAction: A) { return ensuredSet(outerState, reducer(get(outerState), innerAction)); } const traverser = item((state: S) => get(state), update); return definition(type, reduce, assign(traverser, { update })); } export interface At { key: K; action: A; } export function collection( reducerOrProvider: ReducerOrProvider, get: (collection: C, key: K) => I | undefined, set: (collection: C, key: K, item: I) => C ) { const reducer = getReducer(reducerOrProvider); function substitute(col: C, key: K): I { const itemOrMissing = get(col, key); return itemOrMissing === undefined ? reducer.empty : itemOrMissing; } function reduce(col: C, at: At) { return set(col, at.key, reducer(substitute(col, at.key), at.action)); } function update(key: K, action: A): Action<"AT", At> { return { type: "AT", payload: { key, action } }; } function traverser(key: K) { return item((col: C) => substitute(col, key), (action: A) => update(key, action)); } return definition("AT", reduce, assign(traverser, { update })); } export function array(itemReducer: ReducerOrProvider) { return collection( itemReducer, (ar, index) => ar[index], (ar, index, item) => { ar = ar.slice(0); ar[index] = item; return ar; }); } export function objectByString(itemReducer: ReducerOrProvider) { return collection<{ [key: string]: I }, string, I, A>( itemReducer, (obj, key) => obj[key], (obj, key, item) => amend(obj, { [key]: item })); } export function objectByNumber(itemReducer: ReducerOrProvider) { return collection<{ [key: number]: I }, number, I, A>( itemReducer, (obj, key) => obj[key], (obj, key, item) => amend(obj, { [key]: item })); } export function removeFromObjectByString() { return action("REMOVE", (obj: { [key: string]: I }, key: string) => { const clone = assign({}, obj); delete clone[key]; return clone; }); } export function removeFromObjectByNumber() { return action("REMOVE", (obj: { [key: number]: I }, key: number) => { const clone = assign({}, obj); delete clone[key]; return clone; }); } export type Replace = Action<"REPLACE", V>; /** * Defines the reducer for a value that can only be assigned a whole new value. * It only supports the action "REPLACE" whose payload is the replacement value. */ export function primitive() { return reducer(undefined! as V).action(action("REPLACE", (s: V, v: V) => v)); } /** * Action that replaces a whole value, supported by primitives */ export function replace(value: T): Replace { return { type: "REPLACE", payload: value }; } /** * Property is just a type alias for a cursor to a primitive */ export type Property = Cursor>; /** * Defines a property, which is a simple value that can be * replaced with a new value. It uses the primitive reducer. */ export function property( type: T, fetch: (state: S) => V, reduce?: (state: S, payload: V) => S ): ReferenceDefinition> { return reference(type, primitive(), fetch, reduce); } /** * Basic substitute for Object.assign */ export function assign(target: T, source1: S1, source2: S2): T & S1 & S2; export function assign(target: T, source1: S1): T & S1; export function assign(target: T, ...sources: any[]): any { if (!target) { throw new Error("assign's target must be an object") } for (const source of sources) { if (source) { for (const key of Object.keys(source)) { (target as any)[key] = (source as any)[key]; } } } return target; } /** * Pretty good subsitute for object spread syntax. Instead of: * * { ...book, title } * * say: * * amend(book, { title }) */ export function amend(o1: O1, o2: O2) { return assign(Array.isArray(o1) ? [] : {}, o1, o2); } // Oh yes, I went there... var matchFunction = /function\s*[a-z]*\s*\(\s*([a-z]+)\s*\)\s*\{\s*return\s+([a-z]+)\.([a-z]+)/i; var matchLambda = /\(?\s*([a-z]+)\s*\)?\s*\=\>\s*([a-z]+)\.([a-z]+)/i function ensureReducer( context: string, fetch: (state: S) => P, reduce?: (state: S, payload: P) => S ): (state: S, payload: P) => S { if (reduce) { return reduce; } // We might be able to generate reduce by parsing the source of fetch! const src = fetch.toString(); matchFunction.lastIndex = 0; matchLambda.lastIndex = 0; const matched = matchFunction.exec(src) || matchLambda.exec(src) if (!matched) { throw new Error(`Cannot generate reducer for ${context} ` + `- too complex to parse, needs explicit reduce`); } if (matched[1] !== matched[2]) { throw new Error(`Cannot generate reducer for ${context} ` + `- inconsistent parameter usage: ${matched[1]}, ${matched[2]}`); } return (state, value) => amend(state, { [matched[3]]: value }); }