/** * @typed/fp/Use is the only non-referentially transparent module in @typed/fp. It is built atop * of [Ref](./Ref.ts.md) to enable many common workflows. If you're coming from a React background, it is * pretty similar to hooks, but the only constraint is that is should be declared once at the top of the scope of your module. * @since 0.11.0 */ import { disposeBoth, disposeNone } from '@most/disposable' import { Disposable } from '@most/types' import { flow, pipe } from 'fp-ts/function' import * as O from 'fp-ts/Option' import { not } from 'fp-ts/Predicate' import * as E from './Env' import * as EO from './EnvOption' import { alwaysEqualsEq, deepEqualsEq, Eq, EqStrict } from './Eq' import * as KV from './KV' import * as RS from './ReaderStream' import * as Ref from './Ref' import * as RefDisposable from './RefDisposable' import * as R from './Resume' import { delay, SchedulerEnv } from './Scheduler' import * as S from './Stream' /** * Use Refs to check if a value has changed between invocations * @since 0.11.0 * @category Constructor */ export const defaultOptionRef = () => Ref.kv(E.of>(O.none), alwaysEqualsEq) /** * Use Refs to check if a value has changed between invocations * @since 0.11.0 * @category Combinator */ export function useEqWith(ref: Ref.Ref>) { return (Eq: Eq = deepEqualsEq, initial = true) => (value: A): E.Env => pipe( E.Do, E.bindW('previous', () => ref.get), E.bindW('changed', ({ previous }) => pipe( previous, O.matchW(() => initial, not(Eq.equals(value))), E.of, ), ), E.chainW(({ previous, changed }) => pipe( previous, O.matchW( () => E.of(changed), () => pipe(value, O.some, ref.set, E.constant(changed)), ), ), ), ) } /** * Use Refs to check if a value has changed between invocations * @since 0.11.0 * @category Combinator */ export const useEq = (Eq: Eq = deepEqualsEq, initial = true) => useEqWith(defaultOptionRef())(Eq, initial) /** * @since 0.11.0 * @category Options */ export type UseMemoWithOptions = { readonly currentValue: Ref.Ref> readonly changed: Ref.Ref> } /** * @since 0.11.0 * @category Use */ export const useMemoWith = (options: UseMemoWithOptions) => (env: E.Env, Eq: Eq = deepEqualsEq): ((value: B) => E.Env) => { const changed = pipe(Eq, useEqWith(options.changed)) const updateRef = options.currentValue.update(() => EO.fromEnv(env)) return flow( changed, E.chainFirstW((changed) => (changed ? updateRef : E.of(null))), E.chainW(() => options.currentValue.get), EO.getOrElseEW(() => pipe(env, E.chainFirstW(flow(O.some, options.currentValue.set)))), ) } const defaultUseMemoRefs = () => ({ currentValue: defaultOptionRef(), changed: defaultOptionRef(), }) /** * @since 0.11.0 * @category Use */ export const useMemo = (env: E.Env, Eq: Eq = deepEqualsEq) => useMemoWith(defaultUseMemoRefs())(env, Eq) /** * @since 0.11.0 * @category Options */ export type UseDisposableWithOptions = { readonly disposable: Ref.Ref readonly changed: Ref.Ref> } /** * @since 0.11.0 * @category Use */ export const useDisposableWith = (options: UseDisposableWithOptions) => (Eq: Eq = deepEqualsEq, switchLatest = false) => { const changed = useEqWith(options.changed)(Eq) return (f: () => Disposable, value: A): E.Env => pipe( E.Do, E.bindW('changed', () => changed(value)), E.bindW('current', () => options.disposable.get), E.chainW(({ changed, current }) => changed ? pipe( E.fromIO(() => (switchLatest ? current.dispose() : null)), E.chainW(() => E.fromIO(f)), E.chainW((next) => pipe( next, RefDisposable.add, E.map((d) => disposeBoth(d, next)), E.chainW((a) => options.disposable.set(a)), ), ), ) : E.of(current), ), ) } const defaultDisposableRefs = () => ({ disposable: Ref.kv(E.fromIO(disposeNone)), changed: defaultOptionRef(), }) /** * @since 0.11.0 * @category Use */ export const useDisposable = (Eq: Eq = deepEqualsEq, switchLatest = false) => useDisposableWith(defaultDisposableRefs())(Eq, switchLatest) /** * @since 0.11.0 * @category Use */ export const useEffectWith = (options: UseDisposableWithOptions) => { const useD = useDisposableWith(options) return (Eq: Eq = deepEqualsEq, switchLatest = false) => { const use = useD(Eq, switchLatest) return (env: E.Env, value: A) => pipe( E.ask(), E.chainW((r) => use( () => pipe( r, pipe( delay(0), E.chainW(() => env), ), R.exec, ), value, ), ), ) } } /** * @since 0.11.0 * @category Use */ export const useWithPrevious = (ref: Ref.Ref>) => { return (f: (previous: O.Option, value: A) => B, value: A) => pipe( ref.get, E.map((previous) => f(previous, value)), E.chainFirstW(() => pipe(value, O.some, ref.set)), ) } /** * Helps you to convert a Kliesli arrow of an Env into a function to * a Disposable. Useful for UIs where you need to provide onClick={fn} * style handlers. * @since 0.11.0 * @category Use */ export function useEnvK, E1, B, E2>( f: (...args: A) => E.Env, onValue: (value: B) => E.Env = E.of, ): E.Env Disposable> { return pipe( E.Do, E.apSW('refDisposable', RefDisposable.get), E.apSW('resumeF', E.toResumeK(f)), E.apSW('resumeV', E.toResumeK(onValue)), E.map(({ resumeF, resumeV, refDisposable }) => (...args: A) => { const d1 = pipe(resumeF(...args), R.chain(resumeV), R.exec) const d2 = refDisposable.addDisposable(d1) return disposeBoth(d1, d2) }), ) } /** * @since 0.11.0 * @category Use */ export const bindEnvK = ( name: Exclude, f: (...args: Args) => E.Env, onValue?: (value: B) => E.Env, ) => ( ma: E.Env, ): E.Env< E1 & E2 & E3 & KV.Env, { readonly [K in N | keyof A]: K extends keyof A ? A[K] : () => Disposable } > => E.bindW(name, () => useEnvK(f, onValue))(ma) /** * @since 0.11.0 * @category Options */ export type UseReaderStreamWithOptions = { readonly value: Ref.Ref> } & UseDisposableWithOptions /** * @since 0.11.0 * @category Use */ export const useReaderStreamWith = (options: UseReaderStreamWithOptions) => (Eq: Eq = deepEqualsEq) => { const use = useDisposableWith(options)(Eq) return ( rs: RS.ReaderStream, dep: B, ): E.Env> => pipe( E.asksE((r: E1 & E2 & E3 & E4 & SchedulerEnv) => use( () => rs(r).run( S.createSink({ event: (_, value: C) => pipe(value, O.some, options.value.set, E.execWith(r)), }), r.scheduler, ), dep, ), ), E.chainW(() => options.value.get as E.Env>), ) } const defaultUserReaderStreamRefs = () => ({ disposable: Ref.kv(E.fromIO(disposeNone)), changed: defaultOptionRef(), value: defaultOptionRef(), }) /** * @since 0.11.0 * @category Use */ export const useReaderStream = (Eq: Eq = deepEqualsEq) => useReaderStreamWith(defaultUserReaderStreamRefs())(Eq) /** * @since 0.11.0 * @category Options */ export type UseStreamWithOptions = UseReaderStreamWithOptions /** * @since 0.11.0 * @category Use */ export const useStreamWith = (options: UseStreamWithOptions) => (Eq: Eq = deepEqualsEq) => { const useRS = pipe(Eq, useReaderStreamWith(options)) return (stream: S.Stream, dep: B) => useRS(() => stream, dep) } /** * @since 0.11.0 * @category Use */ export const useStream = (Eq: Eq = deepEqualsEq) => { const use = useStreamWith(defaultUserReaderStreamRefs())(Eq) return (stream: S.Stream, dep: A): E.Env> => use(stream, dep) } /** * @since 0.11.0 * @category Use */ export const useKVStream = (f: (value: A) => RS.ReaderStream, Eq: Eq) => { const use = RS.fromEnv(KV.useKeyedEnvs(EqStrict as Eq>)) const mergeMap = RS.mergeMapWhen(EqStrict as Eq>) return ( stream: RS.ReaderStream, ): RS.ReaderStream => pipe( use, RS.switchMapW(({ findRefs, deleteRefs }) => pipe( stream, RS.keyed(Eq), mergeMap((s) => pipe( s, RS.fromStream, RS.switchMapW(f), RS.onDispose(deleteRefs(s)), RS.useSomeWith(RS.fromEnv(findRefs(s))), ), ), ), ), ) } /** * @since 0.11.0 * @category Use */ export const useKVs = (f: (value: A) => E.Env, Eq: Eq) => useKVStream(flow(f, KV.sample), Eq)