|
@@ -7,54 +7,27 @@
|
|
|
|
|
|
import Scheduler from './scheduler'
|
|
|
|
|
|
-class Computation<A> {
|
|
|
- run(ctx?: Computation.Context) {
|
|
|
- return this.runObservable(ctx).result;
|
|
|
- }
|
|
|
-
|
|
|
- runObservable(ctx?: Computation.Context): Computation.Running<A> {
|
|
|
- const context = ctx ? ctx as ObservableContext : new ObservableContext();
|
|
|
-
|
|
|
- return {
|
|
|
- subscribe: (context as ObservableContext).subscribe || 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>) {
|
|
|
-
|
|
|
- }
|
|
|
+interface Computation<A> {
|
|
|
+ run(ctx?: Computation.Context): Promise<A>,
|
|
|
+ runObservable(ctx?: Computation.Context): Computation.Running<A>
|
|
|
}
|
|
|
|
|
|
-
|
|
|
namespace Computation {
|
|
|
export let PRINT_CONSOLE_ERROR = false;
|
|
|
|
|
|
export function create<A>(computation: (ctx: Context) => Promise<A>) {
|
|
|
- return new Computation(computation);
|
|
|
+ return new ComputationImpl(computation);
|
|
|
}
|
|
|
|
|
|
export function resolve<A>(a: A) {
|
|
|
- return new Computation<A>(_ => Promise.resolve(a));
|
|
|
+ return create<A>(_ => Promise.resolve(a));
|
|
|
}
|
|
|
|
|
|
export function reject<A>(reason: any) {
|
|
|
- return new Computation<A>(_ => Promise.reject(reason));
|
|
|
+ return create<A>(_ => Promise.reject(reason));
|
|
|
}
|
|
|
|
|
|
export interface Params {
|
|
|
- isSynchronous: boolean,
|
|
|
updateRateMs: number
|
|
|
}
|
|
|
|
|
@@ -70,14 +43,11 @@ namespace Computation {
|
|
|
}
|
|
|
|
|
|
export interface Context {
|
|
|
+ readonly isSynchronous: boolean,
|
|
|
+ /** Also checks if the computation was aborted. If so, throws. */
|
|
|
readonly requiresUpdate: boolean,
|
|
|
requestAbort(): void,
|
|
|
- /**
|
|
|
- * Checks if the computation was aborted. If so, throws.
|
|
|
- * Otherwise, updates the progress.
|
|
|
- *
|
|
|
- * Returns the number of ms since the last update.
|
|
|
- */
|
|
|
+ /** Also checks if the computation was aborted. If so, throws. */
|
|
|
updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number): Promise<void> | void
|
|
|
}
|
|
|
|
|
@@ -88,7 +58,9 @@ namespace Computation {
|
|
|
result: Promise<A>
|
|
|
}
|
|
|
|
|
|
- export const contextWithoutUpdates: Context = {
|
|
|
+ /** A context without updates. */
|
|
|
+ export const synchronousContext: Context = {
|
|
|
+ isSynchronous: true,
|
|
|
requiresUpdate: false,
|
|
|
requestAbort() { },
|
|
|
updateProgress(msg, abort, current, max) { }
|
|
@@ -98,37 +70,31 @@ namespace Computation {
|
|
|
return new ObservableContext(params);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
declare var process: any;
|
|
|
declare var window: any;
|
|
|
|
|
|
export const now: () => number = (function () {
|
|
|
if (typeof window !== 'undefined' && window.performance) {
|
|
|
const perf = window.performance;
|
|
|
- return function () { return perf.now(); }
|
|
|
+ return () => perf.now();
|
|
|
} else if (typeof process !== 'undefined' && process.hrtime !== 'undefined') {
|
|
|
- return function () {
|
|
|
+ return () => {
|
|
|
let t = process.hrtime();
|
|
|
return t[0] * 1000 + t[1] / 1000000;
|
|
|
};
|
|
|
} else {
|
|
|
- return function () { return +new Date(); }
|
|
|
+ return () => +new Date();
|
|
|
}
|
|
|
- })();
|
|
|
-
|
|
|
- export interface Chunked {
|
|
|
- /**
|
|
|
- * Get automatically computed chunk size
|
|
|
- * Or set it a default value.
|
|
|
- */
|
|
|
- chunkSize: number,
|
|
|
- readonly requiresUpdate: boolean,
|
|
|
- updateProgress: Context['updateProgress'],
|
|
|
- context: Context
|
|
|
+ }());
|
|
|
+
|
|
|
+ /** A utility for splitting large computations into smaller parts. */
|
|
|
+ export interface Chunker {
|
|
|
+ setNextChunkSize(size: number): void,
|
|
|
+ /** nextChunk must return the number of actually processed chunks. */
|
|
|
+ process(nextChunk: (chunkSize: number) => number, update: (updater: Context['updateProgress']) => void, nextChunkSize?: number): Promise<void>
|
|
|
}
|
|
|
|
|
|
- export function chunked(ctx: Context, defaultChunkSize: number): Chunked {
|
|
|
+ export function chunker(ctx: Context, defaultChunkSize: number): Chunker {
|
|
|
return new ChunkedImpl(ctx, defaultChunkSize);
|
|
|
}
|
|
|
}
|
|
@@ -136,9 +102,39 @@ namespace Computation {
|
|
|
const DefaulUpdateRateMs = 150;
|
|
|
const NoOpSubscribe = () => { }
|
|
|
|
|
|
+class ComputationImpl<A> implements Computation<A> {
|
|
|
+ run(ctx?: Computation.Context) {
|
|
|
+ return this.runObservable(ctx).result;
|
|
|
+ }
|
|
|
+
|
|
|
+ runObservable(ctx?: Computation.Context): Computation.Running<A> {
|
|
|
+ const context = ctx ? ctx as ObservableContext : new ObservableContext();
|
|
|
+
|
|
|
+ return {
|
|
|
+ subscribe: (context as ObservableContext).subscribe || 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>) {
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
class ObservableContext implements Computation.Context {
|
|
|
readonly updateRate: number;
|
|
|
- private isSynchronous: boolean;
|
|
|
+ readonly isSynchronous: boolean = false;
|
|
|
private level = 0;
|
|
|
private startedTime = 0;
|
|
|
private abortRequested = false;
|
|
@@ -203,7 +199,7 @@ class ObservableContext implements Computation.Context {
|
|
|
get requiresUpdate() {
|
|
|
this.checkAborted();
|
|
|
if (this.isSynchronous) return false;
|
|
|
- return Computation.now() - this.lastUpdated > this.updateRate / 2;
|
|
|
+ return Computation.now() - this.lastUpdated > this.updateRate;
|
|
|
}
|
|
|
|
|
|
started() {
|
|
@@ -221,43 +217,53 @@ class ObservableContext implements Computation.Context {
|
|
|
|
|
|
constructor(params?: Partial<Computation.Params>) {
|
|
|
this.updateRate = (params && params.updateRateMs) || DefaulUpdateRateMs;
|
|
|
- this.isSynchronous = !!(params && params.isSynchronous);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
-class ChunkedImpl implements Computation.Chunked {
|
|
|
- private currentChunkSize: number;
|
|
|
+class ChunkedImpl implements Computation.Chunker {
|
|
|
+ private processedSinceUpdate = 0;
|
|
|
+ private updater: Computation.Context['updateProgress'];
|
|
|
|
|
|
private computeChunkSize() {
|
|
|
const lastDelta = (this.context as ObservableContext).lastDelta || 0;
|
|
|
- if (!lastDelta) return this.defaultChunkSize;
|
|
|
+ if (!lastDelta) return this.nextChunkSize;
|
|
|
const rate = (this.context as ObservableContext).updateRate || 0;
|
|
|
- return Math.round(this.currentChunkSize * rate / lastDelta + 1);
|
|
|
+ const ret = Math.round(this.processedSinceUpdate * rate / lastDelta + 1);
|
|
|
+ this.processedSinceUpdate = 0;
|
|
|
+ return ret;
|
|
|
}
|
|
|
|
|
|
- get chunkSize() {
|
|
|
- return this.defaultChunkSize;
|
|
|
+ private getNextChunkSize() {
|
|
|
+ const ctx = this.context as ObservableContext;
|
|
|
+ // be smart if the computation is synchronous and process the whole chunk at once.
|
|
|
+ if (ctx.isSynchronous) return Number.MAX_SAFE_INTEGER;
|
|
|
+ return this.nextChunkSize;
|
|
|
}
|
|
|
|
|
|
- set chunkSize(value: number) {
|
|
|
- this.defaultChunkSize = value;
|
|
|
- this.currentChunkSize = value;
|
|
|
+ setNextChunkSize(size: number) {
|
|
|
+ this.nextChunkSize = size;
|
|
|
}
|
|
|
|
|
|
- get requiresUpdate() {
|
|
|
- const ret = this.context.requiresUpdate;
|
|
|
- if (!ret) this.currentChunkSize += this.chunkSize;
|
|
|
- return ret;
|
|
|
- }
|
|
|
+ async process(nextChunk: (size: number) => number, update: (updater: Computation.Context['updateProgress']) => Promise<void> | void, nextChunkSize?: number) {
|
|
|
+ if (typeof nextChunkSize !== 'undefined') this.setNextChunkSize(nextChunkSize);
|
|
|
+ let lastChunk: number;
|
|
|
|
|
|
- async updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number) {
|
|
|
- await this.context.updateProgress(msg, abort, current, max);
|
|
|
- this.defaultChunkSize = this.computeChunkSize();
|
|
|
+ while (( lastChunk = nextChunk(this.getNextChunkSize())) > 0) {
|
|
|
+ this.processedSinceUpdate += lastChunk;
|
|
|
+ if (this.context.requiresUpdate) {
|
|
|
+ await update(this.updater);
|
|
|
+ this.nextChunkSize = this.computeChunkSize();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.context.requiresUpdate) {
|
|
|
+ await update(this.updater);
|
|
|
+ this.nextChunkSize = this.computeChunkSize();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- constructor(public context: Computation.Context, private defaultChunkSize: number) {
|
|
|
- this.currentChunkSize = defaultChunkSize;
|
|
|
+ constructor(public context: Computation.Context, private nextChunkSize: number) {
|
|
|
+ this.updater = this.context.updateProgress.bind(this.context);
|
|
|
}
|
|
|
}
|
|
|
|