sodiumjs
Version:
A Functional Reactive Programming (FRP) library for JavaScript
153 lines (142 loc) • 5.84 kB
text/typescript
import { Vertex, Source } from "./Vertex";
import * as Collections from 'typescript-collections';
import { Stream, StreamWithSend } from "./Stream";
import { StreamSink } from "./StreamSink";
import { Cell } from "./Cell";
import { CellSink } from "./CellSink";
import { Transaction } from "./Transaction";
/**
* An interface for implementations of FRP timer systems.
*/
export abstract class TimerSystemImpl {
/**
* Set a timer that will execute the specified callback at the specified time.
* @return A function that can be used to cancel the timer.
*/
abstract setTimer(t : number, callback : () => void) : () => void;
/**
* Return the current clock time.
*/
abstract now() : number;
}
let nextSeq : number = 0;
class Event {
constructor(t : number, sAlarm : StreamWithSend<number>) {
this.t = t;
this.sAlarm = sAlarm;
this.seq = ++nextSeq;
}
t : number;
sAlarm : StreamWithSend<number>;
seq : number; // Used to guarantee uniqueness
}
export class TimerSystem {
constructor(impl : TimerSystemImpl) {
Transaction.run(() => {
this.impl = impl;
this.tMinimum = 0;
const timeSnk = new CellSink<number>(impl.now());
this.time = timeSnk;
// A dummy listener to time to keep it alive even when there are no other listeners.
this.time.listen((t : number) => { });
Transaction.onStart(() => {
// Ensure the time is always increasing from the FRP's point of view.
const t = this.tMinimum = Math.max(this.tMinimum, impl.now());
// Pop and execute all events earlier than or equal to t (the current time).
while (true) {
let ev : Event = null;
if (!this.eventQueue.isEmpty()) {
let mev = this.eventQueue.minimum();
if (mev.t <= t) {
ev = mev;
// TO DO: Detect infinite loops!
}
}
if (ev != null) {
timeSnk.send(ev.t);
Transaction.run(() => ev.sAlarm.send_(ev.t));
}
else
break;
}
timeSnk.send(t);
});
});
}
private impl : TimerSystemImpl;
private tMinimum : number; // A guard to allow us to guarantee that the time as
// seen by the FRP is always increasing.
/**
* A cell giving the current clock time.
*/
time : Cell<number>;
private eventQueue : Collections.BSTree<Event> = new Collections.BSTree<Event>((a, b) => {
if (a.t < b.t) return -1;
if (a.t > b.t) return 1;
if (a.seq < b.seq) return -1;
if (a.seq > b.seq) return 1;
return 0;
});
/**
* A timer that fires at the specified time, which can be null, meaning
* that the alarm is not set.
*/
at(tAlarm : Cell<number>) : Stream<number> {
let current : Event = null,
cancelCurrent : () => void = null,
active : boolean = false,
tAl : number = null,
sampled : boolean = false;
const sAlarm = new StreamWithSend<number>(null),
updateTimer = () => {
if (cancelCurrent !== null) {
cancelCurrent();
this.eventQueue.remove(current);
}
cancelCurrent = null;
current = null;
if (active) {
if (!sampled) {
sampled = true;
tAl = tAlarm.sampleNoTrans__();
}
if (tAl !== null) {
current = new Event(tAl, sAlarm);
this.eventQueue.add(current);
cancelCurrent = this.impl.setTimer(tAl, () => {
// Correction to ensure the clock time appears to be >= the
// alarm time. It can be a few milliseconds early, and
// this breaks things otherwise, because it doesn't think
// it's time to fire the alarm yet.
this.tMinimum = Math.max(this.tMinimum, tAl);
// Open and close a transaction to trigger queued
// events to run.
Transaction.run(() => {});
});
}
}
};
sAlarm.setVertex__(new Vertex("at", 0, [
new Source(
tAlarm.getVertex__(),
() => {
active = true;
sampled = false;
Transaction.currentTransaction.prioritized(sAlarm.getVertex__(), updateTimer);
const kill = tAlarm.getStream__().listen_(sAlarm.getVertex__(), (oAlarm : number) => {
tAl = oAlarm;
sampled = true;
updateTimer();
}, false);
return () => {
active = false;
updateTimer();
kill();
};
}
)
]
));
return sAlarm;
}
}