sodiumjs
Version:
A Functional Reactive Programming (FRP) library for JavaScript
242 lines (218 loc) • 6.1 kB
text/typescript
import {Vertex} from './Vertex';
import * as Collections from 'typescript-collections';
export class Entry
{
constructor(rank: Vertex, action: () => void)
{
this.rank = rank;
this.action = action;
this.seq = Entry.nextSeq++;
}
private static nextSeq: number = 0;
rank: Vertex;
action: () => void;
seq: number;
toString(): string
{
return this.seq.toString();
}
}
export class Transaction
{
public static currentTransaction: Transaction = null;
private static onStartHooks: (() => void)[] = [];
private static runningOnStartHooks: boolean = false;
constructor() {}
inCallback: number = 0;
private toRegen: boolean = false;
requestRegen(): void
{
this.toRegen = true;
}
prioritizedQ: Collections.PriorityQueue<Entry> = new Collections.PriorityQueue<Entry>((a, b) =>
{
// Note: Low priority numbers are treated as "greater" according to this
// comparison, so that the lowest numbers are highest priority and go first.
if (a.rank.rank < b.rank.rank) return 1;
if (a.rank.rank > b.rank.rank) return -1;
if (a.seq < b.seq) return 1;
if (a.seq > b.seq) return -1;
return 0;
});
private entries: Collections.Set<Entry> = new Collections.Set<Entry>((a) => a.toString());
private sampleQ: Array<() => void> = [];
private lastQ: Array<() => void> = [];
private postQ: Array<() => void> = null;
private static collectCyclesAtEnd: boolean = false;
prioritized(target: Vertex, action: () => void): void
{
const e = new Entry(target, action);
this.prioritizedQ.enqueue(e);
this.entries.add(e);
}
sample(h: () => void): void
{
this.sampleQ.push(h);
}
last(h: () => void): void
{
this.lastQ.push(h);
}
public static _collectCyclesAtEnd(): void
{
Transaction.run(() => Transaction.collectCyclesAtEnd = true);
}
/**
* Add an action to run after all last() actions.
*/
post(childIx: number, action: () => void): void
{
if (this.postQ == null)
this.postQ = [];
// If an entry exists already, combine the old one with the new one.
while (this.postQ.length <= childIx)
this.postQ.push(null);
const existing = this.postQ[childIx],
neu =
existing === null ? action
: () =>
{
existing();
action();
};
this.postQ[childIx] = neu;
}
// If the priority queue has entries in it when we modify any of the nodes'
// ranks, then we need to re-generate it to make sure it's up-to-date.
private checkRegen(): void
{
if (this.toRegen)
{
this.toRegen = false;
this.prioritizedQ.clear();
const es = this.entries.toArray();
for (let i: number = 0; i < es.length; i++)
this.prioritizedQ.enqueue(es[i]);
}
}
public isActive() : boolean
{
return Transaction.currentTransaction ? true : false;
}
close(): void
{
while(true)
{
while (true)
{
this.checkRegen();
if (this.prioritizedQ.isEmpty()) break;
const e = this.prioritizedQ.dequeue();
this.entries.remove(e);
e.action();
}
const sq = this.sampleQ;
this.sampleQ = [];
for (let i = 0; i < sq.length; i++)
sq[i]();
if(this.prioritizedQ.isEmpty() && this.sampleQ.length < 1) break;
}
for (let i = 0; i < this.lastQ.length; i++)
this.lastQ[i]();
this.lastQ = [];
if (this.postQ != null)
{
for (let i = 0; i < this.postQ.length; i++)
{
if (this.postQ[i] != null)
{
const parent = Transaction.currentTransaction;
try
{
if (i > 0)
{
Transaction.currentTransaction = new Transaction();
try
{
this.postQ[i]();
Transaction.currentTransaction.close();
}
catch (err)
{
Transaction.currentTransaction.close();
throw err;
}
}
else
{
Transaction.currentTransaction = null;
this.postQ[i]();
}
Transaction.currentTransaction = parent;
}
catch (err)
{
Transaction.currentTransaction = parent;
throw err;
}
}
}
this.postQ = null;
}
}
/**
* Add a runnable that will be executed whenever a transaction is started.
* That runnable may start transactions itself, which will not cause the
* hooks to be run recursively.
*
* The main use case of this is the implementation of a time/alarm system.
*/
static onStart(r: () => void): void
{
Transaction.onStartHooks.push(r);
}
public static run<A>(f: () => A): A
{
const transWas: Transaction = Transaction.currentTransaction;
if (transWas === null)
{
if (!Transaction.runningOnStartHooks)
{
Transaction.runningOnStartHooks = true;
try
{
for (let i = 0; i < Transaction.onStartHooks.length; i++)
Transaction.onStartHooks[i]();
}
finally
{
Transaction.runningOnStartHooks = false;
}
}
Transaction.currentTransaction = new Transaction();
}
try
{
const a: A = f();
if (transWas === null)
{
Transaction.currentTransaction.close();
Transaction.currentTransaction = null;
if (Transaction.collectCyclesAtEnd) {
Vertex.collectCycles();
Transaction.collectCyclesAtEnd = false;
}
}
return a;
}
catch (err)
{
if (transWas === null)
{
Transaction.currentTransaction.close();
Transaction.currentTransaction = null;
}
throw err;
}
}
}