/** @module hareactive/stream */ import {Reactive, Observer} from "./frp-common"; import {Behavior, at, scan, fromFunction} from "./behavior"; /** * A stream is a list of occurences over time. Each occurence happens * at a discrete point in time and has an associated value. * Semantically it is a list `type Stream = [Time, A]`. */ export abstract class Stream extends Reactive { constructor() { super(); } abstract push(a: any): void; map(fn: (a: A) => B): Stream { const s = new MapStream(fn); this.addListener(s); return s; } mapTo(val: B): Stream { const s = new MapToStream(val); this.addListener(s); return s; } combine(otherStream: Stream): Stream<(A|B)> { const s = new SinkStream<(A|B)>(); this.addListener(s); otherStream.addListener(s); return s; } filter(fn: (a: A) => boolean): Stream { const s = new FilterStream(fn); this.addListener(s); return s; } scanS(fn: (a: A, b: B) => B, startingValue: B): Behavior> { return fromFunction(() => new ScanStream(fn, startingValue, this)); } scan(fn: (a: A, b: B) => B, init: B): Behavior> { return scan(fn, init, this); } delay(ms: number): Stream { const s = new DelayStream(ms); this.addListener(s); return s; } throttle(ms: number): Stream { const s = new ThrottleStream(ms); this.addListener(s); return s; } debounce(ms: number): Stream { const s = new DebounceStream(ms); this.addListener(s); return s; } } /** @private */ export class SinkStream extends Stream { push(a: A): void { this.child.push(a); } } class MapStream extends Stream { constructor(private fn: (a: A) => B) { super(); } push(a: A): void { this.child.push(this.fn(a)); } } class MapToStream extends Stream { constructor(private val: A) { super(); } push(a: any): void { this.child.push(this.val); } } class FilterStream extends Stream { constructor(private fn: (a: A) => boolean) { super(); } push(a: A): void { if (this.fn(a) === true) { this.child.push(a); } } } class DelayStream extends Stream { constructor(private ms: number) { super(); } push(a: A): void { setTimeout(() => this.child.push(a), this.ms); } } class ThrottleStream extends Stream { constructor(private ms: number) { super(); } private isSilenced: boolean = false; push(a: A): void { if (!this.isSilenced) { this.child.push(a); this.isSilenced = true; setTimeout(() => { this.isSilenced = false; }, this.ms); } } } class DebounceStream extends Stream { constructor(private ms: number) { super(); } private timer: number = undefined; push(a: A): void { clearTimeout(this.timer); this.timer = setTimeout(() => { this.child.push(a); }, this.ms); } } export function debounce(ms: number, stream: Stream) { return stream.debounce(ms); } export function throttle(ms: number, stream: Stream) { return stream.throttle(ms); } export function delay(ms: number, stream: Stream) { return stream.delay(ms); } export function apply(behavior: Behavior<(a: A) => B>, stream: Stream): Stream { return stream.map((a: A) => at(behavior)(a)); } /** * @param fn A predicate function that returns a boolean for `A`. * @param stream The stream to filter. * @returns Stream that only contains the occurences from `stream` * for which `fn` returns true. */ export function filter(predicate: (a: A) => boolean, s: Stream): Stream { return s.filter(predicate); } export function split(predicate: (a: A) => boolean, stream: Stream): [Stream, Stream] { // It should be possible to implement this in a faster way where // `predicate` is only called once for each occurrence return [stream.filter(predicate), stream.filter((a) => !predicate(a))]; } export function filterApply(predicate: Behavior<(a: A) => boolean>, stream: Stream): Stream { return stream.filter((a: A) => at(predicate)(a)); } export function keepWhen(stream: Stream, behavior: Behavior): Stream { return stream.filter((_) => at(behavior)); } class ScanStream extends Stream { constructor(private fn: (a: A, b: B) => B, private last: B, source: Stream) { super(); source.addListener(this); } push(a: A): void { const val = this.last = this.fn(a, this.last); this.child.push(val); } } /** * The returned initially has the initial value, on each * occurence in `source` the function is applied to the current value * of the behaviour and the value of the occurence, the returned value * becomes the next value of the behavior. */ export function scanS(fn: (a: A, b: B) => B, startingValue: B, stream: Stream): Behavior> { return stream.scanS(fn, startingValue); } class SnapshotStream extends Stream { constructor(private behavior: Behavior, stream: Stream) { super(); stream.addListener(this); } push(a: any): void { this.child.push(at(this.behavior)); } } export function snapshot(b: Behavior, s: Stream): Stream { return new SnapshotStream(b, s); } class SnapshotWithStream extends Stream { constructor( private fn: (a: A, b: B) => C, private behavior: Behavior, stream: Stream ) { super(); stream.addListener(this); } push(a: A): void { this.child.push(this.fn(a, at(this.behavior))); } } export function snapshotWith( f: (a: A, b: B) => C, b: Behavior, s: Stream ): Stream { return new SnapshotWithStream(f, b, s); } /** @private */ class SwitchOuter implements Observer> { constructor(private s: SwitchBehaviorStream) {}; beginPulling(): void { throw new Error("not implemented"); } endPulling(): void { throw new Error("not implemented"); } push(a: Stream): void { this.s.doSwitch(a); } } class SwitchBehaviorStream extends Stream { private currentSource: Stream; private outerConsumer: Observer>; constructor(private b: Behavior>) { super(); this.outerConsumer = new SwitchOuter(this); b.addListener(this.outerConsumer); const cur = this.currentSource = at(b); cur.addListener(this); } push(a: A): void { this.child.push(a); } public doSwitch(newStream: Stream): void { this.currentSource.removeListener(this); newStream.addListener(this); this.currentSource = newStream; } } export function switchStream(b: Behavior>): Stream { return new SwitchBehaviorStream(b); } class ChangesStream extends Stream { constructor(private b: Behavior) { super(); b.addListener(this); } push(a: A) { this.child.push(a); } beginPulling(): void { throw new Error("Cannot get changes from pulling behavior"); } endPulling(): void { throw new Error("Cannot get changes from pulling behavior"); } } export function changes(b: Behavior): Stream { return new ChangesStream(b); } export class PlaceholderStream extends Stream { private source: Stream; push(a: B): void { this.child.push(a); } replaceWith(s: Stream): void { this.source = s; s.addListener(this); } } export function placeholderStream(): PlaceholderStream { return new PlaceholderStream(); } export function combineList(ss: Stream[]): Stream { // FIXME: More performant implementation with benchmark return ss.reduce((s1, s2) => s1.combine(s2), empty()); } /** * @returns A stream that never has any occurrences. */ export function empty(): Stream { return new SinkStream(); } export function subscribe(fn: (a: A) => void, stream: Stream): void { stream.subscribe(fn); } export function combine(a: Stream, b: Stream): Stream<(A|B)> { return a.combine(b); } export function isStream(s: any): s is Stream { return typeof s === "object" && ("scanS" in s); }