@typed/fp
Version:
Data Structures and Resources for fp-ts
627 lines • 17.3 kB
JavaScript
/**
* Stream is an extension of @most/core with additional
* fp-ts instances as well as additional combinators for interoperation with other data
* structures in @typed/fp and fp-ts.
*
* A large goal of @typed/fp is to expand the `fp-ts` ecosystem to include
* [@most/core](https://github.com/mostjs/core) for a Reactive programming style, including
* derivatives such as [ReaderStream](./ReaderStream.ts.md), [ReaderStreamEither](./ReaderStreamEither.ts.md),
* [StateReaderStreamEither](./StateReaderStreamEither.ts.md) and a few others. It's the fastest push-based
* reactive library in JS period. The performance characteristics are due to it's architecture of getting out of
* the way of the computations you need to perform. It's also the first experience I had with FP. For instance,
* Most utilizes `Functor` laws to remove unneeded machinery through function composition improving runtime
* performance amongst other optimizations.
*
* See the [@most/core Documentation](https://mostcore.readthedocs.io/en/latest/) for the remaining API
* exposed by this module. Both @most/core + @most/types are re-exported from this module
* @since 0.9.2
*/
import * as M from '@most/core';
import { disposeAll, disposeNone } from '@most/disposable';
import { asap, schedulerRelativeTo } from '@most/scheduler';
import * as App from 'fp-ts/Applicative';
import * as Ap from 'fp-ts/Apply';
import * as CH from 'fp-ts/Chain';
import { isLeft, isRight, left, match, right } from 'fp-ts/Either';
import { constVoid, flow, pipe } from 'fp-ts/function';
import { bindTo as bindTo_, tupled as tupled_ } from 'fp-ts/Functor';
import * as O from 'fp-ts/Option';
import * as RA from 'fp-ts/ReadonlyArray';
import * as RM from 'fp-ts/ReadonlyMap';
import { fst, snd } from 'fp-ts/Tuple2';
import { create } from './Adapter';
import { disposeBoth, settable } from './Disposable';
import { deepEqualsEq } from './Eq';
import * as FRe from './FromResume';
import * as R from './Resume';
import * as S from './struct';
/**
* Convert an IO<Disposable> into a Most.js Task
* @since 0.9.2
* @category Constructor
*/
export function createCallbackTask(cb, onError) {
const disposable = settable();
return {
run(t) {
if (!disposable.isDisposed()) {
disposable.addDisposable(cb(t));
}
},
error(_, e) {
disposable.dispose();
if (onError) {
return onError(e);
}
throw e;
},
dispose: disposable.dispose,
};
}
/**
* @since 0.9.2
* @category URI
*/
export const URI = '@most/core/Stream';
/**
* Create a Stream monoid where concat is a parallel merge.
*/
/**
* @since 0.9.2
* @category Typeclass Constructor
*/
export const getMonoid = () => {
return {
concat: M.merge,
empty: M.empty(),
};
};
/**
* Filter Option's from within a Stream
*/
/**
* @since 0.9.2
* @category Combinator
*/
export const compact = (stream) => M.map((s) => s.value, M.filter(O.isSome, stream));
/**
* Separate left and right values
*/
/**
* @since 0.9.2
* @category Combinator
*/
export const separate = (stream) => {
const s = M.multicast(stream);
const left = pipe(s, M.filter(isLeft), M.map((l) => l.left));
const right = pipe(s, M.filter(isRight), M.map((r) => r.right));
return { left, right };
};
/**
* @since 0.9.2
* @category Combinator
*/
export const partitionMap = (f) => (fa) => separate(M.map(f, fa));
/**
* @since 0.9.2
* @category Combinator
*/
export const partition = (predicate) => partitionMap((a) => (predicate(a) ? right(a) : left(a)));
/**
* @since 0.9.2
* @category Combinator
*/
export const filterMap = (f) => (fa) => compact(M.map(f, fa));
/**
* @since 0.9.2
* @category Instance
*/
export const Functor = {
map: M.map,
};
/**
* @since 0.9.2
* @category Instance
*/
export const Pointed = {
of: M.now,
};
/**
* @since 0.9.2
* @category Constructor
*/
export const of = Pointed.of;
/**
* @since 0.9.2
* @category Instance
*/
export const Apply = {
...Functor,
ap: M.ap,
};
/**
* @since 0.9.2
* @category Combinator
*/
export const apFirst = Ap.apFirst(Apply);
/**
* @since 0.9.2
* @category Combinator
*/
export const apS = Ap.apS(Apply);
/**
* @since 0.9.2
* @category Combinator
*/
export const apSecond = Ap.apSecond(Apply);
/**
* @since 0.9.2
* @category Combinator
*/
export const apT = Ap.apT(Apply);
/**
* @since 0.9.2
* @category Typeclass Constructor
*/
export const getApplySemigroup = Ap.getApplySemigroup(Apply);
/**
* @since 0.9.2
* @category Instance
*/
export const Applicative = {
...Apply,
...Pointed,
};
/**
* @since 0.9.2
* @category Typeclass Constructor
*/
export const getApplicativeMonoid = App.getApplicativeMonoid(Applicative);
/**
* @since 0.9.2
* @category Instance
*/
export const Chain = {
...Functor,
chain: M.chain,
};
/**
* @since 0.9.2
* @category Combinator
*/
export const chainFirst = CH.chainFirst(Chain);
/**
* @since 0.9.2
* @category Combinator
*/
export const bind = CH.bind(Chain);
/**
* @since 0.9.2
* @category Instance
*/
export const Monad = {
...Chain,
...Pointed,
};
/**
* @since 0.9.2
* @category Combinator
*/
export const chainRec = (f) => (value) => pipe(value, f, M.delay(0), M.chain(match(flow(chainRec(f)), M.now)));
/**
* @since 0.9.2
* @category Instance
*/
export const ChainRec = {
chainRec,
};
/**
* @since 0.9.2
* @category Combinator
*/
export const switchRec = (f) => (value) => pipe(value, f, M.map(match(switchRec(f), M.now)), M.switchLatest);
/**
* @since 0.9.2
* @category Instance
*/
export const SwitchRec = {
chainRec: switchRec,
};
/**
* @since 0.9.2
* @category Combinator
*/
export const mergeConcurrentlyRec = (concurrency) => (f) => (value) => pipe(value, f, M.map(match(mergeConcurrentlyRec(concurrency)(f), M.now)), M.mergeConcurrently(concurrency));
/**
* @since 0.9.2
* @category Typeclass Constructor
*/
export const getConcurrentChainRec = (concurrency) => ({
chainRec: mergeConcurrentlyRec(concurrency),
});
/**
* @since 0.9.2
* @category Instance
*/
export const FromIO = {
fromIO: (f) => Functor.map(f)(M.now(undefined)),
};
/**
* @since 0.9.2
* @category Constructor
*/
export const fromIO = FromIO.fromIO;
const applyTask = (task) => M.ap(M.now(task), M.now(void 0));
/**
* @since 0.9.2
* @category Instance
*/
export const FromTask = {
...FromIO,
fromTask: flow(applyTask, M.awaitPromises),
};
/**
* @since 0.9.2
* @category Constructor
*/
export const fromTask = FromTask.fromTask;
/**
* @since 0.9.2
* @category Instance
*/
export const FromResume = {
fromResume: (resume) => M.newStream((sink, scheduler) => {
const run = () => pipe(resume, R.start((a) => {
sink.event(scheduler.currentTime(), a);
sink.end(scheduler.currentTime());
}));
const onError = (error) => sink.error(scheduler.currentTime(), error);
return asap(createCallbackTask(run, onError), scheduler);
}),
};
/**
* @since 0.9.2
* @category Constructor
*/
export const fromResume = FromResume.fromResume;
/**
* @since 0.9.2
* @category Combinator
*/
export const chainFirstResumeK = FRe.chainFirstResumeK(FromResume, Chain);
/**
* @since 0.9.2
* @category Combinator
*/
export const chainResumeK = FRe.chainResumeK(FromResume, Chain);
/**
* @since 0.9.2
* @category Constructor
*/
export const fromResumeK = FRe.fromResumeK(FromResume);
/**
* @since 0.9.2
* @category Instance
*/
export const Alt = {
...Functor,
alt: (f) => (fa) => M.take(1, M.merge(fa, f())), // race the 2 streams
};
/**
* @since 0.9.2
* @category Combinator
*/
export const race = Alt.alt;
/**
* @since 0.9.2
* @category Instance
*/
export const Alternative = {
...Alt,
zero: M.empty,
};
/**
* @since 0.9.2
* @category Constructor
*/
export const zero = Alternative.zero;
/**
* @since 0.9.2
* @category Instance
*/
export const Compactable = {
compact,
separate,
};
/**
* @since 0.9.2
* @category Instance
*/
export const Filterable = {
partitionMap,
partition,
filterMap,
filter: M.filter,
};
/**
* @since 0.9.2
* @category Constructor
*/
export const Do = pipe(null, M.now, M.map(Object.create));
/**
* @since 0.9.2
* @category Combinator
*/
export const bindTo = bindTo_(Functor);
/**
* @since 0.9.2
* @category Combinator
*/
export const tupled = tupled_(Functor);
const emptySink = {
event: constVoid,
error: constVoid,
end: constVoid,
};
/**
* @since 0.9.2
* @category Constructor
*/
export const createSink = (sink = {}) => ({
...emptySink,
...sink,
});
/**
* @since 0.9.2
* @category Combinator
*/
export const collectEvents = (scheduler) => (stream) => {
const events = [];
return M.runEffects(M.tap((a) => events.push(a), stream), scheduler).then(() => events);
};
/**
* @since 0.9.2
* @category Combinator
*/
export const onDispose = (disposable) => (stream) => M.newStream((sink, scheduler) => disposeBoth(stream.run(sink, scheduler), disposable));
/**
* @since 0.9.2
* @category Combinator
*/
export const combineAll = (...streams) => pipe(streams, M.combineArray(Array));
/**
* @since 0.11.0
* @category Combinator
*/
export const combineStruct = (streams) => pipe(combineAll(...pipe(Object.entries(streams), RA.map(([k, stream]) => pipe(stream, M.map((v) => S.make(k, v)))))), M.map((o) => Object.assign({}, ...o)));
/**
* @since 0.9.2
* @category Combinator
*/
export const exhaustLatest = (stream) => new ExhaustLatest(stream);
/**
* @since 0.9.2
* @category Combinator
*/
export const exhaustMapLatest = (f) => (stream) => pipe(stream, M.map(f), exhaustLatest);
class ExhaustLatest {
constructor(stream) {
this.stream = stream;
}
run(sink, scheduler) {
const s = new ExhaustLatestSink(sink, scheduler);
return disposeBoth(this.stream.run(s, scheduler), s);
}
}
class ExhaustLatestSink {
constructor(sink, scheduler) {
this.sink = sink;
this.scheduler = scheduler;
this.latest = O.none;
this.disposable = disposeNone();
this.sampling = false;
this.shouldResample = false;
this.finished = false;
this.event = (t, stream) => {
this.latest = O.some(stream);
if (this.sampling) {
this.shouldResample = true;
return;
}
this.sampling = true;
this.shouldResample = false;
this.disposable = stream.run(this.innerSink, schedulerRelativeTo(t, this.scheduler));
};
this.error = (t, e) => {
this.dispose();
this.sink.error(t, e);
};
this.end = (_) => {
this.finished = true;
};
this.dispose = () => {
this.disposable.dispose();
this.sampling = false;
this.shouldResample = false;
};
this.innerSink = {
event: (t, a) => sink.event(t, a),
error: (t, e) => this.error(t, e),
end: (t) => {
this.sampling = false;
if (this.shouldResample && O.isSome(this.latest)) {
this.event(scheduler.currentTime(), this.latest.value);
return;
}
if (this.finished) {
this.dispose();
sink.end(t);
}
},
};
}
}
/**
* Using the provided Eq mergeMapWhen conditionally applies a Kliesli arrow
* to the values within an Array when they are added and any values removed
* from the array will be disposed of immediately
* @since 0.9.2
* @category Combinator
*/
export const mergeMapWhen = (Eq = deepEqualsEq) => (f) => (values) => new MergeMapWhen(Eq, f, values);
class MergeMapWhen {
constructor(Eq, f, values) {
this.Eq = Eq;
this.f = f;
this.values = values;
}
run(sink, scheduler) {
const s = new MergeMapWhenSink(sink, scheduler, this);
return disposeBoth(this.values.run(s, scheduler), s);
}
}
class MergeMapWhenSink {
constructor(sink, scheduler, source) {
this.sink = sink;
this.scheduler = scheduler;
this.source = source;
this.disposables = new Map();
this.values = new Map();
this.current = [];
this.finished = false;
this.referenceCount = 0;
this.disposable = disposeNone();
this.event = (t, values) => {
const removed = pipe(this.current, this.findDifference(values));
const added = pipe(values, this.findDifference(this.current));
this.current = values;
// Clean up all the removed keys
pipe(removed, RA.map(this.findDisposable), RA.compact, disposeAll).dispose();
removed.forEach((r) => this.values.delete(r));
// Subscribe to all the newly added values
added.forEach((a) => this.disposables.set(a, this.runInner(t, a)));
this.emitIfReady();
};
this.error = (t, error) => {
this.dispose();
this.sink.error(t, error);
};
this.end = (t) => {
this.finished = true;
this.endIfReady(t);
};
this.dispose = () => {
this.finished = true;
this.disposables.forEach((d) => d.dispose());
this.disposables.clear();
this.values.clear();
this.current = [];
};
this.runInner = (t, v) => this.source.f(v).run(this.innerSink(v), schedulerRelativeTo(t, this.scheduler));
this.innerSink = (v) => {
this.referenceCount++;
return {
event: (_, a) => {
this.values.set(v, a);
this.emitIfReady();
},
error: (t, e) => this.error(t, e),
end: (t) => {
this.referenceCount--;
this.endIfReady(t);
},
};
};
this.emitIfReady = () => {
const values = pipe(this.current, RA.map(this.findValue), RA.compact);
if (values.length === this.current.length) {
this.sink.event(this.scheduler.currentTime(), values);
}
};
this.endIfReady = (t) => {
if (this.finished && this.referenceCount === 0) {
this.sink.end(t);
this.dispose();
}
};
this.findDisposable = (k) => RM.lookup(source.Eq)(k)(this.disposables);
this.findValue = (k) => RM.lookup(source.Eq)(k)(this.values);
this.findDifference = RA.difference(source.Eq);
}
}
/**
* @since 0.9.2
* @category Combinator
*/
export const keyed = (Eq) => (stream) => new Keyed(Eq, stream);
class Keyed {
constructor(Eq, stream) {
this.Eq = Eq;
this.stream = stream;
}
run(sink, scheduler) {
const s = new KeyedSink(this.Eq, sink, scheduler);
return disposeBoth(s, this.stream.run(s, scheduler));
}
}
class KeyedSink {
constructor(Eq, sink, scheduler) {
this.Eq = Eq;
this.sink = sink;
this.scheduler = scheduler;
this.adapters = new Map();
this.current = [];
this.event = (t, values) => {
// Clean up after any removed values
const removed = pipe(this.current, this.findDifference(values));
removed.forEach((r) => {
// Send the end signal
this.getAdapter(r)[1][0](null);
// Delete
this.adapters.delete(r);
});
this.current = values;
// Emit our latest set of streams
this.sink.event(t, values.map((a) => pipe(a, this.getAdapter, fst, snd)));
values.forEach((a) => pipe(a, this.getAdapter, fst, fst)(a));
};
this.getAdapter = (k) => {
return pipe(k, this.findAdapter, O.getOrElseW(() => {
const endSignal = create();
const values = create(flow(M.startWith(k), M.until(endSignal[1]), M.multicast));
const adapter = [values, endSignal];
this.adapters.set(k, adapter);
return adapter;
}));
};
this.error = (t, err) => {
this.dispose();
this.sink.error(t, err);
};
this.end = (t) => {
this.dispose();
this.sink.end(t);
};
this.dispose = () => {
this.current = [];
this.adapters.clear();
};
this.findDifference = RA.difference(Eq);
this.findAdapter = (k) => RM.lookup(Eq)(k)(this.adapters);
}
}
/**
* @since 0.9.2
* @category Combinator
*/
export const switchFirst = (second) => (first) => pipe(first, M.map((a) => pipe(second, M.constant(a), M.startWith(a))), M.switchLatest);
/**
* @since 0.9.2
* @category Combinator
*/
export function mergeFirst(a) {
return (b) => pipe(pipe(a, M.constant(O.none)), M.merge(pipe(b, M.map(O.some))), compact);
}
export * from '@most/core';
export * from '@most/types';
//# sourceMappingURL=Stream.js.map