import {Utils} from "../Utils"; export class UIEventSource { public data: T; private readonly tag: string; private _callbacks = []; private static allSources : UIEventSource[] = UIEventSource.PrepPerf(); static PrepPerf() : UIEventSource[]{ if(Utils.runningFromConsole){ return []; } // @ts-ignore window.mapcomplete_performance = () => { console.log(UIEventSource.allSources.length, "uieventsources created"); const copy = [...UIEventSource.allSources]; copy.sort((a,b) => b._callbacks.length - a._callbacks.length); console.log("Topten is:") for (let i = 0; i < 10; i++) { console.log(copy[i].tag, copy[i]); } return UIEventSource.allSources; } return []; } constructor(data: T, tag: string = "") { this.tag = tag; this.data = data; UIEventSource.allSources.push(this); } public static flatten(source: UIEventSource>, possibleSources: UIEventSource[]): UIEventSource { const sink = new UIEventSource(source.data?.data); source.addCallback((latestData) => { sink.setData(latestData?.data); }); for (const possibleSource of possibleSources) { possibleSource?.addCallback(() => { sink.setData(source.data?.data); }) } return sink; } public static Chronic(millis: number, asLong: () => boolean = undefined): UIEventSource { const source = new UIEventSource(undefined); function run() { source.setData(new Date()); if (asLong === undefined || asLong()) { window.setTimeout(run, millis); } } run(); return source; } public addCallback(callback: ((latestData: T) => void)): UIEventSource { if (callback === console.log) { // This ^^^ actually works! throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." } this._callbacks.push(callback); return this; } public addCallbackAndRun(callback: ((latestData: T) => void)): UIEventSource { callback(this.data); return this.addCallback(callback); } public setData(t: T): UIEventSource { if (this.data == t) { // MUST COMPARE BY REFERENCE! return; } this.data = t; this.ping(); return this; } public ping(): void { for (const callback of this._callbacks) { callback(this.data); } } /** * Monoidal map: * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' * @param f: The transforming function * @param extraSources: also trigger the update if one of these sources change * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData */ public map(f: ((t: T) => J), extraSources: UIEventSource[] = [], g: ((j:J, t:T) => T) = undefined): UIEventSource { const self = this; const newSource = new UIEventSource( f(this.data), "map("+this.tag+")" ); const update = function () { newSource.setData(f(self.data)); } this.addCallbackAndRun(update); for (const extraSource of extraSources) { extraSource?.addCallback(update); } if (g !== undefined) { newSource.addCallback((latest) => { self.setData(g(latest, self.data)); }) } return newSource; } public syncWith(otherSource: UIEventSource, reverseOverride = false): UIEventSource { this.addCallback((latest) => otherSource.setData(latest)); const self = this; otherSource.addCallback((latest) => self.setData(latest)); if (reverseOverride && otherSource.data !== undefined) { this.setData(otherSource.data); } else if (this.data === undefined) { this.setData(otherSource.data); } else { otherSource.setData(this.data); } return this; } public stabilized(millisToStabilize): UIEventSource { const newSource = new UIEventSource(this.data); let currentCallback = 0; this.addCallback(latestData => { currentCallback++; const thisCallback = currentCallback; window.setTimeout(() => { if (thisCallback === currentCallback) { newSource.setData(latestData); } }, millisToStabilize) }); return newSource; } }