diff --git a/rollup.config.js b/rollup.config.js index 577c39346dfe9d6ac093ea293e2f87985223cb97..cae10ad5d9564a165d3f79716974e314de857041 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -32,5 +32,9 @@ export default { // } ], external: external, - sourcemap: false + sourcemap: false, + onwarn(warning, warn) { + if (warning.code === 'THIS_IS_UNDEFINED') return; + warn(warning); // this requires Rollup 0.46 + } }; \ No newline at end of file diff --git a/src/script.ts b/src/script.ts index ba82c95d2522de77b017782b321e37100dfe51ac..e792fdc922f494cd137e667017be6cdd9b7ca541 100644 --- a/src/script.ts +++ b/src/script.ts @@ -118,4 +118,20 @@ export function _cif() { }); } -_cif(); \ No newline at end of file +_cif(); + +import Computation from './utils/computation' +const comp = new Computation(async ctx => { + for (let i = 0; i < 3; i++) { + await new Promise(res => setTimeout(res, 500)); + if (ctx.requiresUpdate) await ctx.updateProgress('working', void 0, i, 2); + } + return 42; +}); +async function testComp() { + const running = comp.runWithContext(); + running.subscribe(p => console.log(JSON.stringify(p))); + const ret = await running.result; + console.log('computation returned', ret); +} +testComp(); \ No newline at end of file diff --git a/src/utils/computation.ts b/src/utils/computation.ts new file mode 100644 index 0000000000000000000000000000000000000000..0206208cc4d48eed1100d6f7a5b655d5479cfd12 --- /dev/null +++ b/src/utils/computation.ts @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from https://github.com/dsehnal/LiteMol + * @author David Sehnal <david.sehnal@gmail.com> + */ + +class Computation<A> { + run(ctx?: Computation.Context) { + return this.runWithContext(ctx).result; + } + + runWithContext(ctx?: Computation.Context): Computation.Running<A> { + const context = ctx ? ctx as Computation.ObservableContext : new Computation.ObservableContext(); + + return { + subscribe: (context as Computation.ObservableContext).subscribe || Helpers.NoOpSubscribe, + result: new Promise<A>(async (resolve, reject) => { + try { + if (context.started) context.started(); + const result = await this.computation(context); + resolve(result); + } catch (e) { + if (Computation.PRINT_CONSOLE_ERROR) console.error(e); + reject(e); + } finally { + if (context.finished) context.finished(); + } + }) + }; + } + + constructor(private computation: (ctx: Computation.Context) => Promise<A>) { + + } +} + +namespace Computation { + const DefaulUpdateRateMs = 100; + + export let PRINT_CONSOLE_ERROR = false; + + export function resolve<A>(a: A) { + return new Computation<A>(() => Promise.resolve(a)); + } + + export function reject<A>(reason: any) { + return new Computation<A>(() => Promise.reject(reason)); + } + + export interface Params { + isSynchronous: boolean, + updateRateMs: number + } + + export const Aborted = 'Aborted'; + + export interface Progress { + message: string; + isIndeterminate: boolean; + current: number; + max: number; + requestAbort?: () => void; + } + + export interface Context { + readonly requiresUpdate: boolean, + requestAbort(): void, + /** + * Checks if the computation was aborted. If so, throws. + * Otherwise, updates the progress. + */ + updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number): Promise<void> | void + } + + type ProgressObserver = (progress: Readonly<Progress>) => void; + + export interface Running<A> { + subscribe(onProgress: ProgressObserver): void; + result: Promise<A> + } + + export const ContextWithoutUpdates: Context = { + requiresUpdate: false, + requestAbort() { }, + updateProgress(msg, abort, current, max) { } + } + + export class ObservableContext implements Context { + private updateRate: number; + private isSynchronous: boolean; + private level = 0; + private abortRequested = false; + private lastUpdated = 0; + private observers: ProgressObserver[] | undefined = void 0; + private progress: Progress = { message: 'Working...', current: 0, max: 0, isIndeterminate: true, requestAbort: void 0 }; + + private checkAborted() { + if (this.abortRequested) throw Aborted; + } + + private abortRequester = () => { this.abortRequested = true }; + + subscribe = (obs: ProgressObserver) => { + if (!this.observers) this.observers = []; + this.observers.push(obs); + } + + requestAbort() { + try { + if (this.abortRequester) { + this.abortRequester.call(null); + } + } catch (e) { } + } + + updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number): Promise<void> | void { + this.checkAborted(); + + if (typeof abort === 'boolean') { + this.progress.requestAbort = abort ? this.abortRequester : void 0; + } else { + if (abort) this.abortRequester = abort; + this.progress.requestAbort = abort ? this.abortRequester : void 0; + } + + this.progress.message = msg; + if (isNaN(current!)) { + this.progress.isIndeterminate = true; + } else { + this.progress.isIndeterminate = false; + this.progress.current = current!; + this.progress.max = max!; + } + + if (this.observers) { + const p = { ...this.progress }; + for (const o of this.observers) setTimeout(o, 0, p); + } + + this.lastUpdated = Helpers.getTime(); + + return new Promise<void>(res => setTimeout(res, 0)); + } + + get requiresUpdate() { + this.checkAborted(); + if (this.isSynchronous) return false; + return Helpers.getTime() - this.lastUpdated > this.updateRate; + } + + started() { + this.level++; + } + + finished() { + this.level--; + if (this.level < 0) { + throw new Error('Bug in code somewhere, Computation.resolve/reject called too many times.'); + } + if (!this.level) this.observers = void 0; + } + + constructor(params?: Params) { + this.updateRate = (params && params.updateRateMs) || DefaulUpdateRateMs; + this.isSynchronous = !!(params && params.isSynchronous); + } + } +} + +namespace Helpers { + declare var process: any; + declare var window: any; + + export const getTime: () => number = (function () { + if (typeof window !== 'undefined' && window.performance) { + const perf = window.performance; + return function () { return perf.now(); } + } else if (typeof process !== 'undefined' && process.hrtime !== 'undefined') { + return function () { + let t = process.hrtime(); + return t[0] * 1000 + t[1] / 1000000; + }; + } else { + return function () { return +new Date(); } + } + })(); + + export const NoOpSubscribe = () => {}; +} + +export default Computation; \ No newline at end of file