@typed/fp
Version:
Data Structures and Resources for fp-ts
503 lines (435 loc) • 10.1 kB
text/typescript
/**
*
* `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<A> = Sync<A> | Async<A>
/**
* @since 0.9.2
* @category Type-level
*/
export type ValueOf<A> = [A] extends [Resume<infer R>] ? R : never
/**
* @since 0.9.2
* @category Model
*/
export interface Async<A> {
readonly _tag: 'async'
readonly resume: AsyncResume<A>
}
/**
* @since 0.9.2
* @category Model
*/
export type AsyncResume<A> = (resume: (value: A) => Disposable) => Disposable
/**
* @since 0.9.2
* @category Constructor
*/
export const async = <A>(resume: AsyncResume<A>): Async<A> => ({ _tag: 'async', resume })
/**
* @since 0.9.2
* @category Constructor
*/
export const fromTask = <A>(task: Task<A>): Async<A> =>
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<A> {
readonly _tag: 'sync'
readonly resume: IO<A>
}
/**
* @since 0.9.2
* @category Constructor
*/
export const sync = <A>(resume: IO<A>): Sync<A> => ({ _tag: 'sync', resume })
/**
* @since 0.9.2
* @category Refinement
*/
export const isSync = <A>(resume: Resume<A>): resume is Sync<A> => resume._tag === 'sync'
/**
* @since 0.9.2
* @category Refinement
*/
export const isAsync = <A>(resume: Resume<A>): resume is Async<A> => resume._tag === 'async'
/**
* @since 0.9.2
* @category Combinator
*/
export const ap =
<A>(fa: Resume<A>) =>
<B>(fab: Resume<Arity1<A, B>>): Resume<B> => {
if (isSync(fa) && isSync(fab)) {
return sync(() => fab.resume()(fa.resume()))
}
// Concurrently
return async((resume) => {
const disposable = settable()
let ab: Option<Arity1<A, B>> = isSync(fab) ? some(fab.resume()) : none
let a: Option<A> = 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 =
<A>(f: Arity1<A, Disposable>) =>
(resume: Resume<A>): Disposable =>
isAsync(resume) ? resume.resume(f) : f(resume.resume())
/**
* @since 0.9.2
* @category Deconstructor
*/
export const start = <A>(f: Arity1<A, any>) => run(undisposable(f))
/**
* @since 0.9.2
* @category Deconstructor
*/
export const exec = start<any>(constVoid)
/**
* @since 0.9.2
* @category Model
*/
export type DisposableTask<A> = () => DisposablePromise<A>
/**
* @since 0.9.2
* @category Model
*/
export type DisposablePromise<A> = Promise<A> & Disposable
/**
* @since 0.9.2
* @category Deconstructor
*/
export const toTask = <A>(resume: Resume<A>): DisposableTask<A> => {
const task = () => {
const d = settable()
const p = new Promise<A>((resolve) => d.addDisposable(pipe(resume, start(resolve))))
;(p as Promise<A> & Disposable).dispose = () => d.dispose()
return p as Promise<A> & Disposable
}
return task
}
/**
* @since 0.9.2
* @category Combinator
*/
export const chain =
<A, B>(f: Arity1<A, Resume<B>>) =>
(resume: Resume<A>): Resume<B> =>
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 =
<A, B>(f: Arity1<A, Resume<E.Either<A, B>>>) =>
(value: A): Resume<B> => {
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 =
<A>(ra: Resume<A>) =>
<B>(rb: Resume<B>): Resume<A | B> => {
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<A> {
[URI]: Resume<A>
}
}
/**
* @since 0.9.2
* @category Instance
*/
export const Pointed: Pointed1<URI> = {
of,
}
/**
* @since 0.9.2
* @category Instance
*/
export const Functor: Functor1<URI> = {
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<URI> = {
...Functor,
ap,
}
/**
* @since 0.9.2
* @category Instance
*/
export const Applicative: Applicative1<URI> = {
...Apply,
...Pointed,
}
/**
* @since 0.9.2
* @category Instance
*/
export const Chain: Ch.Chain1<URI> = {
...Functor,
chain,
}
/**
* @since 0.9.2
* @category Combinator
*/
export const chainFirst = Ch.chainFirst(Chain)
/**
* @since 0.9.2
* @category Instance
*/
export const Monad: Monad1<URI> = {
...Chain,
...Pointed,
}
/**
* @since 0.9.2
* @category Instance
*/
export const ChainRec: ChainRec1<URI> = {
URI,
chainRec,
}
/**
* @since 0.9.2
* @category Instance
*/
export const MonadRec: MonadRec1<URI> = {
...Monad,
chainRec,
}
/**
* @since 0.9.2
* @category Instance
*/
export const Alt: Alt1<URI> = {
...Functor,
alt: (snd) => (fst) => pipe(fst, race(snd())),
}
/**
* @since 0.9.2
* @category Instance
*/
export const FromIO: FromIO1<URI> = {
URI,
fromIO: sync,
}
/**
* @since 0.9.2
* @category Constructor
*/
export const fromIO = FromIO.fromIO
/**
* @since 0.9.2
* @category Instance
*/
export const FromTask: FromTask1<URI> = {
...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(<A>(x: Resume<A>) => 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)