/** @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);
}