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 = new Collections.PriorityQueue((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 = new Collections.Set((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(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; } } }