/** * * `Resume` is a possibly synchronous or asynchronous effect type. Conceptually you can think of * `Resume` as a union between the `IO` and `Task` monads w/ cancelation. The `Disposable` interface is * borrowed from `@most/core`'s internal types and makes it easy to interoperate. Using `Disposable` * makes it easy to reuse the `@most/scheduler` package for providing scheduling consistent with any * other most-based workflows you have. * * The beauty of `Resume` is that it can do work as fast as possible without delay. Sync when possible * but async when needed allows you to unify your sync/async workflows using a single monad w/ easy * interop with your existing IO/Task allowing for cancelation when required. * @since 0.9.2 */ import { Disposable } from '@most/types' import { Alt1 } from 'fp-ts/Alt' import { Applicative1, getApplicativeMonoid } from 'fp-ts/Applicative' import { Apply1 } from 'fp-ts/Apply' import * as Ch from 'fp-ts/Chain' import { ChainRec1 } from 'fp-ts/ChainRec' import * as E from 'fp-ts/Either' import * as FIO from 'fp-ts/FromIO' import { FromIO1 } from 'fp-ts/FromIO' import * as FT from 'fp-ts/FromTask' import { FromTask1 } from 'fp-ts/FromTask' import { constant, constVoid, flow, pipe } from 'fp-ts/function' import { bindTo as bindTo_, Functor1, tupled as tupled_ } from 'fp-ts/Functor' import { IO } from 'fp-ts/IO' import { Monad1 } from 'fp-ts/Monad' import { isNone, none, Option, some } from 'fp-ts/Option' import { Pointed1 } from 'fp-ts/Pointed' import * as RA from 'fp-ts/ReadonlyArray' import { Task } from 'fp-ts/Task' import { disposeBoth, disposeNone, settable, undisposable } from './Disposable' import { Arity1 } from './function' import { MonadRec1 } from './MonadRec' /** * @since 0.9.2 * @category Model */ export type Resume = Sync | Async /** * @since 0.9.2 * @category Type-level */ export type ValueOf = [A] extends [Resume] ? R : never /** * @since 0.9.2 * @category Model */ export interface Async { readonly _tag: 'async' readonly resume: AsyncResume } /** * @since 0.9.2 * @category Model */ export type AsyncResume = (resume: (value: A) => Disposable) => Disposable /** * @since 0.9.2 * @category Constructor */ export const async = (resume: AsyncResume): Async => ({ _tag: 'async', resume }) /** * @since 0.9.2 * @category Constructor */ export const fromTask = (task: Task): Async => async((resume) => { const disposable = settable() task().then((r) => { if (!disposable.isDisposed()) { disposable.addDisposable(resume(r)) } }) return disposable }) /** * @since 0.9.2 * @category Model */ export interface Sync { readonly _tag: 'sync' readonly resume: IO } /** * @since 0.9.2 * @category Constructor */ export const sync = (resume: IO): Sync => ({ _tag: 'sync', resume }) /** * @since 0.9.2 * @category Refinement */ export const isSync = (resume: Resume): resume is Sync => resume._tag === 'sync' /** * @since 0.9.2 * @category Refinement */ export const isAsync = (resume: Resume): resume is Async => resume._tag === 'async' /** * @since 0.9.2 * @category Combinator */ export const ap = (fa: Resume) => (fab: Resume>): Resume => { if (isSync(fa) && isSync(fab)) { return sync(() => fab.resume()(fa.resume())) } // Concurrently return async((resume) => { const disposable = settable() let ab: Option> = isSync(fab) ? some(fab.resume()) : none let a: Option = isSync(fa) ? some(fa.resume()) : none function onReady() { if (isNone(ab) || isNone(a)) { return disposeNone() } if (!disposable.isDisposed()) { disposable.addDisposable(resume(ab.value(a.value))) } return disposable } if (isAsync(fab)) { disposable.addDisposable( fab.resume((f) => { ab = some(f) return onReady() }), ) } if (isAsync(fa)) { disposable.addDisposable( fa.resume((x) => { a = some(x) return onReady() }), ) } return disposable }) } /** * @since 0.9.2 * @category Deconstructor */ export const run = (f: Arity1) => (resume: Resume): Disposable => isAsync(resume) ? resume.resume(f) : f(resume.resume()) /** * @since 0.9.2 * @category Deconstructor */ export const start = (f: Arity1) => run(undisposable(f)) /** * @since 0.9.2 * @category Deconstructor */ export const exec = start(constVoid) /** * @since 0.9.2 * @category Model */ export type DisposableTask = () => DisposablePromise /** * @since 0.9.2 * @category Model */ export type DisposablePromise = Promise & Disposable /** * @since 0.9.2 * @category Deconstructor */ export const toTask = (resume: Resume): DisposableTask => { const task = () => { const d = settable() const p = new Promise((resolve) => d.addDisposable(pipe(resume, start(resolve)))) ;(p as Promise & Disposable).dispose = () => d.dispose() return p as Promise & Disposable } return task } /** * @since 0.9.2 * @category Combinator */ export const chain = (f: Arity1>) => (resume: Resume): Resume => isSync(resume) ? f(resume.resume()) : async((r) => resume.resume(flow(f, run(r)))) /** * @since 0.9.2 * @category Constructor */ export const of = flow(constant, sync) /** * @since 0.9.2 * @category Combinator */ export const chainRec = (f: Arity1>>) => (value: A): Resume => { let resume = f(value) while (isSync(resume)) { const either = resume.resume() if (E.isRight(either)) { return of(either.right) } resume = f(either.left) } // Recursion is okay because Resume SHOULD be asynchronous return pipe(resume, chain(E.match(chainRec(f), of))) } /** * @since 0.9.2 * @category Combinator */ export const race = (ra: Resume) => (rb: Resume): Resume => { if (isSync(rb)) { return rb } if (isSync(ra)) { return ra } return async((resume) => { const aDisposableLazy = settable() const bDisposable = pipe( rb, run((b) => { aDisposableLazy.dispose() return resume(b) }), ) const aDisposable = pipe( ra, run((a) => { bDisposable.dispose() return resume(a) }), ) aDisposableLazy.addDisposable(aDisposable) return disposeBoth(bDisposable, aDisposable) }) } /** * @since 0.9.2 * @category URI */ export const URI = '@typed/fp/Resume' /** * @since 0.9.2 * @category URI */ export type URI = typeof URI declare module 'fp-ts/HKT' { export interface URItoKind { [URI]: Resume } } /** * @since 0.9.2 * @category Instance */ export const Pointed: Pointed1 = { of, } /** * @since 0.9.2 * @category Instance */ export const Functor: Functor1 = { URI, map: (f) => (fa) => pipe(fa, chain(flow(f, of))), } /** * @since 0.9.2 * @category Combinator */ export const map = Functor.map /** * @since 0.9.2 * @category Instance */ export const Apply: Apply1 = { ...Functor, ap, } /** * @since 0.9.2 * @category Instance */ export const Applicative: Applicative1 = { ...Apply, ...Pointed, } /** * @since 0.9.2 * @category Instance */ export const Chain: Ch.Chain1 = { ...Functor, chain, } /** * @since 0.9.2 * @category Combinator */ export const chainFirst = Ch.chainFirst(Chain) /** * @since 0.9.2 * @category Instance */ export const Monad: Monad1 = { ...Chain, ...Pointed, } /** * @since 0.9.2 * @category Instance */ export const ChainRec: ChainRec1 = { URI, chainRec, } /** * @since 0.9.2 * @category Instance */ export const MonadRec: MonadRec1 = { ...Monad, chainRec, } /** * @since 0.9.2 * @category Instance */ export const Alt: Alt1 = { ...Functor, alt: (snd) => (fst) => pipe(fst, race(snd())), } /** * @since 0.9.2 * @category Instance */ export const FromIO: FromIO1 = { URI, fromIO: sync, } /** * @since 0.9.2 * @category Constructor */ export const fromIO = FromIO.fromIO /** * @since 0.9.2 * @category Instance */ export const FromTask: FromTask1 = { ...FromIO, fromTask, } /** * @since 0.9.2 * @category Constructor */ export const Do: Resume<{}> = sync(() => Object.create(null)) /** * @since 0.9.2 * @category Combinator */ export const bindTo = bindTo_(Functor) /** * @since 0.9.2 * @category Combinator */ export const bind = Ch.bind(Monad) /** * @since 0.9.2 * @category Combinator */ export const tupled = tupled_(Functor) /** * @since 0.9.2 * @category Combinator */ export const traverseReadonlyArray = RA.traverse(Applicative) /** * @since 0.9.2 * @category Combinator */ export const traverseReadonlyArrayWithIndex = RA.traverseWithIndex(Applicative) /** * @since 0.9.2 * @category Combinator */ export const zip = traverseReadonlyArray((x: Resume) => x) /** * @since 0.9.2 * @category Combinator */ export const chainFirstTaskK = FT.chainFirstTaskK(FromTask, Chain) /** * @since 0.9.2 * @category Combinator */ export const chainTaskK = FT.chainTaskK(FromTask, Chain) /** * @since 0.9.2 * @category Constructor */ export const fromTaskK = FT.fromTaskK(FromTask) /** * @since 0.9.2 * @category Combinator */ export const chainFirstIOK = FIO.chainFirstIOK(FromIO, Chain) /** * @since 0.9.2 * @category Combinator */ export const chainIOK = FIO.chainIOK(FromIO, Chain) /** * @since 0.9.2 * @category Constructor */ export const fromIOK = FIO.fromIOK(FromIO) /** * @since 0.9.2 * @category Typeclass Constructor */ export const getMonoid = getApplicativeMonoid(Applicative)