import {F1, Curried, List, Option} from "functools-ts" import {Observable} from "rxjs" import {map} from "rxjs/operators" export type DispatchOpts = { tag?: string, noReplay?: boolean, takeLatest?: boolean } export type Continuation = S | Promise> | Observable> export type UpdateFn = F1> export type Transitions = List> export type Update = (UpdateFn | Transitions) & DispatchOpts export const defer = (f: () => void): Promise => new Promise((resolve, reject) => setTimeout(() => { try { f() resolve() } catch (ex) { reject(ex) } } )) /* * dispatch(transitions(,,,,,,,,,,,,,,,,)) * * */ export const DispatchSymbol = Symbol("Dispatch") export type Dispatch = (( update: Update, opts?: DispatchOpts ) => void) & {[DispatchSymbol]: boolean} export interface Dispatcher { dispatch: Dispatch } export type Get = F1 export type Set = Curried type AsyncCalls = {[key: string]: number} export interface AsyncCallTracker { latestTimestampRecorded(name: string): number record(name: string): number } /** * @description test if continuation is a promise * instanceof on promise is not reliable to test if a continuation is a promise * otherwise we test the constructor name and at last resort we test * if the continuation looks like a promise * @param cont */ export const isPromise = ( cont: Continuation ): cont is Promise> => { const contAny = cont as any return contAny instanceof Promise || (contAny.constructor && contAny.constructor.name === "Promise") || (typeof contAny.then === "function" && typeof contAny.catch === "function") } export const isObservable = ( cont: Continuation ): cont is Observable> => !!(cont as any).subscribe export const isDispatch = (obj: any): obj is Dispatch => !!obj[DispatchSymbol] export const nullDispatch = ((_: UpdateFn) => {}) as Dispatch nullDispatch[DispatchSymbol] = true export interface GetAndSet { get: Get set: Set } export const isTransitions = (update: Update): update is Transitions => update instanceof Array const mapUpdateTo = (lens: GetAndSet) => (updateFn: UpdateFn): UpdateFn => state => { const cont = updateFn(lens.get(state)) if (isPromise(cont)) return cont.then(pupdate => (state: S) => lens.set(pupdate(lens.get(state)))(state) ) else if (isObservable(cont)) { return cont.pipe( map(pupdate => (state: S) => { const newS1 = pupdate(lens.get(state)) return lens.set(newS1)(state) }) ) } return lens.set(cont)(state) } export const childDispatchFromLens = ( parentDispatch: Dispatch, lens: GetAndSet ): Dispatch => { let dispatch = ((( update: Update, opts?: DispatchOpts ) => { const optsParam: DispatchOpts = { tag: opts && opts.tag ? opts.tag : update.tag, noReplay: opts && opts.noReplay ? opts.noReplay : update.noReplay, takeLatest: opts && opts.takeLatest && opts.tag ? opts.takeLatest : update.takeLatest } if (isTransitions(update)) { parentDispatch( update.map(mapUpdateTo(lens)), optsParam ) } else { parentDispatch( mapUpdateTo(lens)(update), optsParam ) } }) as any) as Dispatch dispatch[DispatchSymbol] = true return dispatch } export const childDispatch = ( parentDispatch: Dispatch, key: K ): Dispatch => childDispatchFromLens(parentDispatch, { get: s => s[key], set: s1 => s => ({ ...s, [key]: s1 }) }) export const childDispatchFromIndex = (parentDispatch: Dispatch, key: K, idx: number): Dispatch => childDispatchFromLens(parentDispatch, { get: (state) => (state[key] as any)[idx], set: (item) => state => ({ ...state, [key]: List.set(state[key] as any, idx, item) }) }) export const asyncCallTracker = (): AsyncCallTracker => { let asyncCalls: AsyncCalls = {} return { latestTimestampRecorded(name: string): number { if (Option.isEmpty(asyncCalls[name])) throw new Error(`No async call found for name: ${name} this is probably a bug in dispatch-react`) return asyncCalls[name] }, record(name: string): number { if (Option.isEmpty(asyncCalls[name])) asyncCalls[name] = 0 else if (asyncCalls[name] === Number.MAX_SAFE_INTEGER) asyncCalls[name] = 0 else asyncCalls[name] += 1 return asyncCalls[name] } } } export const takeLatest = (update: Update, name: string): Update => { update.tag = name update.takeLatest = true return update } export const disableReplay = (update: Update): Update => { update.noReplay = false return update } export const transitions = (...updates: UpdateFn[]): Transitions => updates export const tag = (update: Update, name: string): Update => { update.tag = name return update }