Browse Source

Merge branch 'master' into mol2-2

Zepei Xu 7 years ago
parent
commit
f0b7aa0775

+ 123 - 30
package-lock.json

@@ -5,9 +5,9 @@
   "requires": true,
   "dependencies": {
     "@types/benchmark": {
-      "version": "1.0.30",
-      "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.30.tgz",
-      "integrity": "sha1-9r6gus6xeBVZaLmYT5RUb2NndZI=",
+      "version": "1.0.31",
+      "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.31.tgz",
+      "integrity": "sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA==",
       "dev": true
     },
     "@types/body-parser": {
@@ -17,7 +17,7 @@
       "dev": true,
       "requires": {
         "@types/express": "4.0.39",
-        "@types/node": "8.0.47"
+        "@types/node": "8.0.56"
       }
     },
     "@types/express": {
@@ -37,13 +37,13 @@
       "integrity": "sha512-/0nwIzF1Bd4KGwW4lhDZYi5StmCZG1DIXXMfQ/zjORzlm4+F1eRA4c6yJQrt4hqX//TDtPULpSlYwmSNyCMeMg==",
       "dev": true,
       "requires": {
-        "@types/node": "8.0.47"
+        "@types/node": "8.0.56"
       }
     },
     "@types/jest": {
-      "version": "21.1.5",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-21.1.5.tgz",
-      "integrity": "sha512-HwBIPK96DROvcB4EX5mA7L2nzhZ3sh8AcDSFODB4eG43S3q3d0+oo356J51qCxw9Bbn7G1DOmBmrw6vf+WiGTg==",
+      "version": "21.1.8",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-21.1.8.tgz",
+      "integrity": "sha512-hQbL8aBM/g5S++sM1gb4yC73Dg+FK3uYE+Ioht1RPy629+LV/RmH6q+e+jbQEwKJdWAP/YE4s67CPO+ElkMivg==",
       "dev": true
     },
     "@types/mime": {
@@ -53,9 +53,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "8.0.47",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.47.tgz",
-      "integrity": "sha512-kOwL746WVvt/9Phf6/JgX/bsGQvbrK5iUgzyfwZNcKVFcjAUVSpF9HxevLTld2SG9aywYHOILj38arDdY1r/iQ==",
+      "version": "8.0.56",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.56.tgz",
+      "integrity": "sha512-JAlQv3hUWbrnruuTiLDf1scd4F/TBT0LgGEe+BBeF3p/Rc3yL6RV57WJN2nK5i+BshEz1sDllwH0Fzbuo7G4QA==",
       "dev": true
     },
     "@types/node-fetch": {
@@ -64,7 +64,7 @@
       "integrity": "sha1-UhB46PDGmhWOUCIAWsqS0mIPbVc=",
       "dev": true,
       "requires": {
-        "@types/node": "8.0.47"
+        "@types/node": "8.0.56"
       }
     },
     "@types/serve-static": {
@@ -240,6 +240,12 @@
       "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=",
       "dev": true
     },
+    "array-filter": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
+      "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
+      "dev": true
+    },
     "array-find-index": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
@@ -251,6 +257,18 @@
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
       "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
     },
+    "array-map": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+      "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
+      "dev": true
+    },
+    "array-reduce": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+      "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
+      "dev": true
+    },
     "array-unique": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
@@ -988,6 +1006,25 @@
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
       "dev": true
     },
+    "cpx": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz",
+      "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "chokidar": "1.7.0",
+        "duplexer": "0.1.1",
+        "glob": "7.1.2",
+        "glob2base": "0.0.12",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "resolve": "1.1.7",
+        "safe-buffer": "5.1.1",
+        "shell-quote": "1.6.1",
+        "subarg": "1.0.0"
+      }
+    },
     "cross-spawn": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -1263,6 +1300,12 @@
         "meow": "3.7.0"
       }
     },
+    "duplexer": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+      "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+      "dev": true
+    },
     "duplexer3": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -1638,6 +1681,12 @@
         "unpipe": "1.0.0"
       }
     },
+    "find-index": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz",
+      "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=",
+      "dev": true
+    },
     "find-up": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@@ -1696,9 +1745,9 @@
       "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
     },
     "fs-extra": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz",
-      "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=",
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
+      "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
       "dev": true,
       "requires": {
         "graceful-fs": "4.1.11",
@@ -1787,6 +1836,15 @@
         "is-glob": "2.0.1"
       }
     },
+    "glob2base": {
+      "version": "0.0.12",
+      "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz",
+      "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=",
+      "dev": true,
+      "requires": {
+        "find-index": "0.1.1"
+      }
+    },
     "globals": {
       "version": "9.18.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
@@ -3842,9 +3900,9 @@
       }
     },
     "rollup": {
-      "version": "0.50.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.50.0.tgz",
-      "integrity": "sha512-7RqCBQ9iwsOBPkjYgoIaeUij606mSkDMExP0NT7QDI3bqkHYQHrQ83uoNIXwPcQm/vP2VbsUz3kiyZZ1qPlLTQ==",
+      "version": "0.50.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.50.1.tgz",
+      "integrity": "sha512-XwrnqjSTk+yR8GbP6hiJuVe83MVmBw/gm4P3qP34A10fRXvv6ppl0ZUg1+Pj1tIZSR/aw5ZaILLEiVxwXIAdAw==",
       "dev": true
     },
     "rollup-plugin-buble": {
@@ -4067,6 +4125,18 @@
       "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
       "dev": true
     },
+    "shell-quote": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
+      "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
+      "dev": true,
+      "requires": {
+        "array-filter": "0.0.1",
+        "array-map": "0.0.0",
+        "array-reduce": "0.0.0",
+        "jsonify": "0.0.0"
+      }
+    },
     "shellwords": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
@@ -4277,6 +4347,23 @@
         "escape-string-regexp": "1.0.5"
       }
     },
+    "subarg": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+      "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+      "dev": true,
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
     "supports-color": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
@@ -4384,18 +4471,18 @@
       "dev": true
     },
     "ts-jest": {
-      "version": "21.1.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-21.1.4.tgz",
-      "integrity": "sha512-dq5JvwHnxwsQa36dNGOQEpHRcJAZubOvgDfKIQww1L4PUzYTTZOFL2VreyhiUfkOVDir77p3nTQSrpvCv1iFTg==",
+      "version": "21.2.4",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-21.2.4.tgz",
+      "integrity": "sha512-Plk49Us+DcncpQcC8fhYwDUdhW96QB0Dv02etOLhzq+2HAvXfrEUys3teZ/BeyQ+r1rHxfGdNj4dB0Q5msZR3g==",
       "dev": true,
       "requires": {
         "babel-core": "6.26.0",
         "babel-plugin-istanbul": "4.1.5",
         "babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
         "babel-preset-jest": "21.2.0",
-        "fs-extra": "4.0.2",
+        "cpx": "1.5.0",
+        "fs-extra": "4.0.3",
         "jest-config": "21.2.1",
-        "jest-util": "21.2.1",
         "pkg-dir": "2.0.0",
         "source-map-support": "0.5.0",
         "yargs": "10.0.3"
@@ -4566,21 +4653,27 @@
       }
     },
     "typescript": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz",
-      "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=",
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
+      "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=",
       "dev": true
     },
     "uglify-js": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.1.7.tgz",
-      "integrity": "sha512-ZRs/A6LJhLac7SN3Hv22LXEtndFliqTkfTilYSx84cxHXoC2bbhdSOsUZqRvfwEAjlIE4AK8pLd47+RxINCTjg==",
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.2.1.tgz",
+      "integrity": "sha512-BhZTJPmOKPSUcjnx2nlfaOQKHLyjjT4HFyzFWF1BUErx9knJNpdW94ql5o8qVxeNL+8IAWjEjnPvASH2yZnkMg==",
       "dev": true,
       "requires": {
-        "commander": "2.11.0",
+        "commander": "2.12.2",
         "source-map": "0.6.1"
       },
       "dependencies": {
+        "commander": {
+          "version": "2.12.2",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
+          "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==",
+          "dev": true
+        },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

+ 7 - 7
package.json

@@ -33,24 +33,24 @@
   "author": "",
   "license": "MIT",
   "devDependencies": {
-    "@types/benchmark": "^1.0.30",
+    "@types/benchmark": "^1.0.31",
     "@types/express": "^4.0.39",
-    "@types/jest": "^21.1.5",
-    "@types/node": "^8.0.47",
+    "@types/jest": "^21.1.8",
+    "@types/node": "^8.0.56",
     "@types/node-fetch": "^1.6.7",
     "benchmark": "^2.1.4",
     "download-cli": "^1.0.5",
     "jest": "^21.2.1",
-    "rollup": "^0.50.0",
+    "rollup": "^0.50.1",
     "rollup-plugin-buble": "^0.16.0",
     "rollup-plugin-commonjs": "^8.2.6",
     "rollup-plugin-json": "^2.3.0",
     "rollup-plugin-node-resolve": "^3.0.0",
     "rollup-watch": "^4.3.1",
-    "ts-jest": "^21.1.4",
+    "ts-jest": "^21.2.4",
     "tslint": "^5.8.0",
-    "typescript": "^2.6.1",
-    "uglify-js": "^3.1.7",
+    "typescript": "^2.6.2",
+    "uglify-js": "^3.2.1",
     "util.promisify": "^1.0.0"
   },
   "dependencies": {

+ 81 - 1
src/examples/computation.ts

@@ -1 +1,81 @@
-// TODO
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task, Run, Progress, Scheduler, now } from 'mol-task'
+
+export async function test1() {
+    const t = Task.create('test', async () => 1);
+    const r = await Run(t);
+    console.log(r);
+}
+
+function messageTree(root: Progress.Node, prefix = ''): string {
+    if (!root.children.length) return `${prefix}${root.progress.taskName}: ${root.progress.message}`;
+
+    const newPrefix = prefix + '  |_ ';
+    const subTree = root.children.map(c => messageTree(c, newPrefix));
+    return `${prefix}${root.progress.taskName}: ${root.progress.message}\n${subTree.join('\n')}`;
+}
+
+function createTask<T>(delayMs: number, r: T): Task<T> {
+    return Task.create('delayed value ' + r, async ctx => {
+        ctx.update('Processing delayed... ' + r, true);
+        await Scheduler.delay(delayMs);
+        if (ctx.shouldUpdate) await ctx.update({ message: 'hello from delayed... ' });
+        return r;
+    }, () => console.log('On abort called ' + r));
+}
+
+export function abortAfter(delay: number) {
+    return Task.create('abort after ' + delay, async ctx => {
+        await Scheduler.delay(delay);
+        throw Task.Aborted('test');
+        //if (ctx.shouldUpdate) await ctx.update({ message: 'hello from delayed... ' });
+        //return r;
+    });
+}
+
+function testTree() {
+    return Task.create('test o', async ctx => {
+        await Scheduler.delay(250);
+        if (ctx.shouldUpdate) await ctx.update({ message: 'hi! 1' });
+        await Scheduler.delay(125);
+        if (ctx.shouldUpdate) await ctx.update({ message: 'hi! 2' });
+        await Scheduler.delay(250);
+        if (ctx.shouldUpdate) await ctx.update('hi! 3');
+
+        // ctx.update('Running children...', true);
+        const c1 = ctx.runChild(createTask(250, 1));
+        const c2 = ctx.runChild(createTask(500, 2));
+        const c3 = ctx.runChild(createTask(750, 3));
+
+        //await ctx.runChild(abortAfter(350));
+
+        const r = await c1 + await c2 + await c3;
+        if (ctx.shouldUpdate) await ctx.update({ message: 'Almost done...' });
+        return r + 1;
+    });
+}
+
+export function abortingObserver(p: Progress) {
+    console.log(messageTree(p.root));
+    if (now() - p.root.progress.startedTime > 1000) {
+        p.requestAbort('test');
+    }
+}
+
+async function test() {
+    try {
+        //const r = await Run(testTree(), p => console.log(messageTree(p.root)), 250);
+        const r = await Run(testTree(), abortingObserver, 250);
+        console.log(r);
+    } catch (e) {
+        console.error(e);
+    }
+}
+
+test();
+//testObs();

+ 9 - 0
src/mol-data/generic.ts

@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export * from './generic/hash-set'
+export * from './generic/linked-list'
+export * from './generic/unique-array'

+ 63 - 0
src/mol-data/generic/_spec/linked-list.spec.ts

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { LinkedList } from '../linked-list'
+
+describe('linked list', () => {
+
+    function toArray<T>(list: LinkedList<T>) {
+        const ret: T[] = [];
+        for (let t = list.first; !!t; t = t.next) {
+            ret[ret.length] = t.value;
+        }
+        return ret;
+    }
+
+    function create<T>(xs: T[]) {
+        const list = LinkedList<T>();
+        for (const x of xs) list.addLast(x);
+        return list;
+    }
+
+    it('add', () => {
+        const list = LinkedList<number>();
+        list.addFirst(1);
+        list.addLast(2);
+        list.addFirst(3);
+        list.addFirst(4);
+        list.addLast(5);
+        expect(toArray(list)).toEqual([4, 3, 1, 2, 5]);
+        expect(list.count).toBe(5);
+    });
+
+    it ('remove', () => {
+        const list = create([1, 2, 3, 4]);
+        let fst = list.removeFirst();
+        expect(fst).toBe(1);
+        expect(list.last!.value).toBe(4);
+        expect(list.count).toBe(3);
+        expect(toArray(list)).toEqual([2, 3, 4]);
+
+        let last = list.removeLast();
+        expect(last).toBe(4);
+        expect(list.last!.value).toBe(3);
+        expect(list.count).toBe(2);
+        expect(toArray(list)).toEqual([2, 3]);
+
+        let n3 = list.find(3)!;
+        list.remove(n3);
+        expect(list.first!.value).toBe(2);
+        expect(list.last!.value).toBe(2);
+        expect(list.count).toBe(1);
+        expect(toArray(list)).toEqual([2]);
+
+        list.removeFirst();
+        expect(list.first).toBe(null);
+        expect(list.last).toBe(null);
+        expect(list.count).toBe(0);
+        expect(toArray(list)).toEqual([]);
+    })
+});

+ 0 - 0
src/mol-data/util/hash-set.ts → src/mol-data/generic/hash-set.ts


+ 119 - 0
src/mol-data/generic/linked-list.ts

@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+interface LinkedList<T> {
+    readonly count: number,
+    readonly first: LinkedList.Node<T> | null,
+    readonly last: LinkedList.Node<T> | null,
+    addFirst(value: T): LinkedList.Node<T>,
+    addLast(value: T): LinkedList.Node<T>,
+    remove(node: LinkedList.Node<T>): void,
+    removeFirst(): T | undefined,
+    removeLast(): T | undefined,
+    find(value: T): LinkedList.Node<T> | undefined
+}
+
+function LinkedList<T>(): LinkedList<T> {
+    return new LinkedListImpl();
+}
+
+namespace LinkedList {
+    export interface Node<T> {
+        previous: Node<T> | null,
+        next: Node<T> | null,
+        inList: boolean,
+        value: T
+    }
+}
+
+function createListNode<T>(value: T): LinkedList.Node<T> {
+    return { previous: null, next: null, inList: true, value };
+}
+
+class LinkedListImpl<T> implements LinkedList<T> {
+    count: number = 0;
+    first: LinkedList.Node<T> | null = null;
+    last: LinkedList.Node<T> | null = null;
+
+    addFirst(value: T) {
+        const node = createListNode(value);
+        node.inList = true;
+        if (this.first) this.first.previous = node;
+        node.next = this.first;
+        this.first = node;
+        this.count++;
+        if (!this.last) this.last = node;
+        return node;
+    }
+
+    addLast(value: T) {
+        const node = createListNode(value);
+        if (this.last !== null) {
+            this.last.next = node;
+        }
+        node.previous = this.last;
+        this.last = node;
+        if (this.first === null) {
+            this.first = node;
+        }
+        node.inList = true;
+        this.count++;
+        return node;
+    }
+
+    removeFirst(): T | undefined {
+        const fst = this.first;
+        if (fst) {
+            this.remove(fst);
+            return fst.value;
+        }
+        return void 0;
+    }
+
+    removeLast(): T | undefined {
+        const last = this.last;
+        if (last) {
+            this.remove(last);
+            return last.value;
+        }
+        return void 0;
+    }
+
+    remove(node: LinkedList.Node<T>) {
+        if (!node.inList) return;
+
+        node.inList = false;
+
+        if (node.previous !== null) {
+            node.previous.next = node.next;
+        }
+        else if (/*first == item*/ node.previous === null) {
+            this.first = node.next;
+        }
+
+        if (node.next !== null) {
+            node.next.previous = node.previous;
+        }
+        else if (/*last == item*/ node.next === null) {
+            this.last = node.previous;
+        }
+
+        node.next = null;
+        node.previous = null;
+        this.count--;
+    }
+
+    find(value: T): LinkedList.Node<T> | undefined {
+        let current = this.first;
+        while (current !== null) {
+            if (current.value === value) return current;
+            current = current.next;
+        }
+        return void 0;
+    }
+}
+
+export { LinkedList }

+ 0 - 0
src/mol-data/util/unique-array.ts → src/mol-data/generic/unique-array.ts


+ 2 - 1
src/mol-data/index.ts

@@ -8,5 +8,6 @@ import * as DB from './db'
 import * as Int from './int'
 import Iterator from './iterator'
 import * as Util from './util'
+import * as Generic from './generic'
 
-export { DB, Int, Iterator, Util }
+export { DB, Int, Iterator, Util, Generic }

+ 0 - 2
src/mol-data/util.ts

@@ -5,8 +5,6 @@
  */
 
 export * from './util/chunked-array'
-export * from './util/unique-array'
-export * from './util/hash-set'
 export * from './util/equivalence-classes'
 export * from './util/hash-functions'
 export * from './util/sort'

+ 1 - 1
src/mol-data/util/array.ts

@@ -14,7 +14,7 @@ export function arrayFind<T>(array: ArrayLike<T>, f: (v: T) => boolean): T | und
 
 export function iterableToArray<T>(it: IterableIterator<T>): T[] {
     if (Array.from) return Array.from(it);
-    
+
     const ret = [];
     while (true) {
         const { done, value } = it.next();

+ 1 - 1
src/mol-model/structure/query/selection.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { HashSet } from 'mol-data/util'
+import { HashSet } from 'mol-data/generic'
 import { Structure, AtomSet } from '../structure'
 
 // A selection is a pair of a Structure and a sequence of unique AtomSets

+ 1 - 1
src/mol-model/structure/structure/structure.ts

@@ -5,7 +5,7 @@
  */
 
 import { OrderedSet, Iterator } from 'mol-data/int'
-import { UniqueArray } from 'mol-data/util'
+import { UniqueArray } from 'mol-data/generic'
 import SymmetryOperator from 'mol-math/geometry/symmetry-operator'
 import { Model, Format } from '../model'
 import Unit from './unit'

+ 0 - 283
src/mol-task/computation.ts

@@ -1,283 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * Adapted from https://github.com/dsehnal/LiteMol
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import Scheduler from './scheduler'
-import timeNow from './time'
-
-interface Computation<A> {
-    (ctx?: Computation.Context): Promise<A>
-}
-
-namespace Computation {
-    export let PRINT_ERRORS_TO_CONSOLE = false;
-
-    export function create<A>(computation: (ctx: Context) => Promise<A>) {
-        return ComputationImpl(computation);
-    }
-
-    export function resolve<A>(a: A) {
-        return create<A>(_ => Promise.resolve(a));
-    }
-
-    export function reject<A>(reason: any) {
-        return create<A>(_ => Promise.reject(reason));
-    }
-
-    export interface Params {
-        updateRateMs?: number,
-        observer?: ProgressObserver
-    }
-
-    export const Aborted = 'Aborted';
-
-    export interface Progress {
-        message: string,
-        isIndeterminate: boolean,
-        current: number,
-        max: number,
-        elapsedMs: number,
-        requestAbort?: () => void
-    }
-
-    export interface ProgressUpdate {
-        message?: string,
-        abort?: boolean | (() => void),
-        current?: number,
-        max?: number
-    }
-
-    export interface Context {
-        readonly isSynchronous: boolean,
-        /** Also checks if the computation was aborted. If so, throws. */
-        readonly requiresUpdate: boolean,
-        requestAbort(): void,
-
-        subscribe(onProgress: ProgressObserver): { dispose: () => void },
-        /** Also checks if the computation was aborted. If so, throws. */
-        update(info: ProgressUpdate): Promise<void> | void
-    }
-
-    export type ProgressObserver = (progress: Readonly<Progress>) => void;
-
-    const emptyDisposer = { dispose: () => { } }
-
-    /** A context without updates. */
-    export const synchronous: Context = {
-        isSynchronous: true,
-        requiresUpdate: false,
-        requestAbort() { },
-        subscribe(onProgress) { return emptyDisposer; },
-        update(info) { }
-    }
-
-    export function observable(params?: Partial<Params>) {
-        const ret = new ObservableContext(params && params.updateRateMs);
-        if (params && params.observer) ret.subscribe(params.observer);
-        return ret;
-    }
-
-    export const now = timeNow;
-
-    /** 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['update']) => void, nextChunkSize?: number): Promise<void>
-    }
-
-    export function chunker(ctx: Context, nextChunkSize: number): Chunker {
-        return new ChunkerImpl(ctx, nextChunkSize);
-    }
-}
-
-const DefaulUpdateRateMs = 150;
-
-function ComputationImpl<A>(computation: (ctx: Computation.Context) => Promise<A>): Computation<A> {
-    return (ctx?: Computation.Context) => {
-        const context: ObservableContext = ctx ? ctx : Computation.synchronous as any;
-        return new Promise<A>(async (resolve, reject) => {
-            try {
-                if (context.started) context.started();
-                const result = await computation(context);
-                resolve(result);
-            } catch (e) {
-                if (Computation.PRINT_ERRORS_TO_CONSOLE) console.error(e);
-                reject(e);
-            } finally {
-                if (context.finished) context.finished();
-            }
-        });
-    }
-}
-
-class ObservableContext implements Computation.Context {
-    readonly updateRate: number;
-    readonly isSynchronous: boolean = false;
-    private level = 0;
-    private startedTime = 0;
-    private abortRequested = false;
-    private lastUpdated = 0;
-    private observers: Computation.ProgressObserver[] | undefined = void 0;
-    private progress: Computation.Progress = { message: 'Working...', current: 0, max: 0, elapsedMs: 0, isIndeterminate: true, requestAbort: void 0 };
-
-    private checkAborted() {
-        if (this.abortRequested) throw Computation.Aborted;
-    }
-
-    private abortRequester = () => { this.abortRequested = true };
-
-    subscribe = (obs: Computation.ProgressObserver) => {
-        if (!this.observers) this.observers = [];
-        this.observers.push(obs);
-        return {
-            dispose: () => {
-                if (!this.observers) return;
-                for (let i = 0; i < this.observers.length; i++) {
-                    if (this.observers[i] === obs) {
-                        this.observers[i] = this.observers[this.observers.length - 1];
-                        this.observers.pop();
-                        return;
-                    }
-                }
-            }
-        };
-    }
-
-    requestAbort() {
-        try {
-            if (this.abortRequester) {
-                this.abortRequester.call(null);
-            }
-        } catch (e) { }
-    }
-
-    update({ message, abort, current, max }: Computation.ProgressUpdate): Promise<void> | void {
-        this.checkAborted();
-
-        const time = Computation.now();
-
-        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;
-        }
-
-        if (typeof message !== 'undefined') this.progress.message = message;
-        this.progress.elapsedMs = time - this.startedTime;
-        if (isNaN(current!)) {
-            this.progress.isIndeterminate = true;
-        } else {
-            this.progress.isIndeterminate = false;
-            this.progress.current = current!;
-            if (!isNaN(max!)) this.progress.max = max!;
-        }
-
-        if (this.observers) {
-            const p = { ...this.progress };
-            for (let i = 0, _i = this.observers.length; i < _i; i++) {
-                Scheduler.immediate(this.observers[i], p);
-            }
-        }
-
-        this.lastUpdated = time;
-
-        return Scheduler.immediatePromise();
-    }
-
-    get requiresUpdate() {
-        this.checkAborted();
-        if (this.isSynchronous) return false;
-        return Computation.now() - this.lastUpdated > this.updateRate;
-    }
-
-    started() {
-        if (!this.level) {
-            this.startedTime = Computation.now();
-            this.lastUpdated = this.startedTime;
-        }
-        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(updateRate?: number) {
-        this.updateRate = updateRate || DefaulUpdateRateMs;
-    }
-}
-
-class ChunkerImpl implements Computation.Chunker {
-    private processedSinceUpdate = 0;
-    private updater: Computation.Context['update'];
-
-    private computeChunkSize(delta: number) {
-        if (!delta) {
-            this.processedSinceUpdate = 0;
-            return this.nextChunkSize;
-        }
-        const rate = (this.context as ObservableContext).updateRate || DefaulUpdateRateMs;
-        const ret = Math.round(this.processedSinceUpdate * rate / delta + 1);
-        this.processedSinceUpdate = 0;
-        return ret;
-    }
-
-    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;
-    }
-
-    setNextChunkSize(size: number) {
-        this.nextChunkSize = size;
-    }
-
-    async process(nextChunk: (size: number) => number, update: (updater: Computation.Context['update']) => Promise<void> | void, nextChunkSize?: number) {
-        if (typeof nextChunkSize !== 'undefined') this.setNextChunkSize(nextChunkSize);
-        this.processedSinceUpdate = 0;
-
-        // track time for the actual computation and exclude the "update time"
-        let chunkStart = Computation.now();
-        let lastChunkSize: number;
-        let chunkCount = 0;
-        let totalSize = 0;
-        let updateCount = 0;
-        while ((lastChunkSize = nextChunk(this.getNextChunkSize())) > 0) {
-            chunkCount++;
-            this.processedSinceUpdate += lastChunkSize;
-            totalSize += lastChunkSize;
-            if (this.context.requiresUpdate) {
-                let time = Computation.now();
-                await update(this.updater);
-                this.nextChunkSize = updateCount > 0
-                    ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
-                    : this.computeChunkSize(time - chunkStart)
-                updateCount++;
-                chunkStart = Computation.now();
-            }
-        }
-        if (this.context.requiresUpdate) {
-            let time = Computation.now();
-            await update(this.updater);
-            this.nextChunkSize = updateCount > 0
-                ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
-                : this.computeChunkSize(time - chunkStart)
-        }
-    }
-
-    constructor(public context: Computation.Context, private nextChunkSize: number) {
-        this.updater = this.context.update.bind(this.context);
-    }
-}
-
-export default Computation;

+ 225 - 0
src/mol-task/execution/observable.ts

@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../task'
+import { RuntimeContext } from './runtime-context'
+import { Progress } from './progress'
+import { now } from '../util/now'
+import { Scheduler } from '../util/scheduler'
+
+function ExecuteObservable<T>(task: Task<T>, observer: Progress.Observer, updateRateMs = 250) {
+    const info = ProgressInfo(task, observer, updateRateMs);
+    const ctx = new ObservableRuntimeContext(info, info.root);
+    return execute(task, ctx);
+}
+
+namespace ExecuteObservable {
+    export let PRINT_ERRORS_TO_STD_ERR = false;
+}
+
+function defaultProgress(task: Task<any>): Task.Progress {
+    return {
+        taskId: task.id,
+        taskName: task.name,
+        message: '',
+        startedTime: 0,
+        canAbort: true,
+        isIndeterminate: true,
+        current: 0,
+        max: 0
+    };
+}
+
+interface ProgressInfo {
+    updateRateMs: number,
+    lastNotified: number,
+    observer: Progress.Observer,
+
+    abortToken: { abortRequested: boolean, treeAborted: boolean, reason: string },
+    taskId: number;
+    root: Progress.Node;
+    tryAbort: (reason?: string) => void;
+}
+
+function ProgressInfo(task: Task<any>, observer: Progress.Observer, updateRateMs: number): ProgressInfo {
+    const abortToken: ProgressInfo['abortToken'] = { abortRequested: false, treeAborted: false, reason: '' };
+
+    return {
+        updateRateMs,
+        lastNotified: now(),
+        observer,
+        abortToken,
+        taskId: task.id,
+        root: { progress: defaultProgress(task), children: [] },
+        tryAbort: createAbortFunction(abortToken)
+    };
+}
+
+function createAbortFunction(token: ProgressInfo['abortToken']) {
+    return (reason?: string) => {
+        token.abortRequested = true;
+        token.reason = reason || token.reason;
+    };
+}
+
+function cloneTree(root: Progress.Node): Progress.Node {
+    return { progress: { ...root.progress }, children: root.children.map(cloneTree) };
+}
+
+function canAbort(root: Progress.Node): boolean {
+    return root.progress.canAbort && root.children.every(canAbort);
+}
+
+function snapshotProgress(info: ProgressInfo): Progress {
+    return { root: cloneTree(info.root), canAbort: canAbort(info.root), requestAbort: info.tryAbort };
+}
+
+async function execute<T>(task: Task<T>, ctx: ObservableRuntimeContext) {
+    ctx.node.progress.startedTime = now();
+    try {
+        const ret = await task.__f(ctx);
+        if (ctx.info.abortToken.abortRequested) {
+            abort(ctx.info, ctx.node);
+        }
+        return ret;
+    } catch (e) {
+        if (Task.isAbort(e)) {
+            // wait for all child computations to go thru the abort phase.
+            if (ctx.node.children.length > 0) {
+                await new Promise(res => { ctx.onChildrenFinished = res; });
+            }
+            if (task.__onAbort) task.__onAbort();
+        }
+        if (ExecuteObservable.PRINT_ERRORS_TO_STD_ERR) console.error(e);
+        throw e;
+    }
+}
+
+function abort(info: ProgressInfo, node: Progress.Node) {
+    if (!info.abortToken.treeAborted) {
+        info.abortToken.treeAborted = true;
+        abortTree(info.root);
+        notifyObserver(info, now());
+    }
+
+    throw Task.Aborted(info.abortToken.reason);
+}
+
+function abortTree(root: Progress.Node) {
+    const progress = root.progress;
+    progress.isIndeterminate = true;
+    progress.canAbort = false;
+    progress.message = 'Aborting...';
+    for (const c of root.children) abortTree(c);
+}
+
+function shouldNotify(info: ProgressInfo, time: number) {
+    return time - info.lastNotified > info.updateRateMs;
+}
+
+function notifyObserver(info: ProgressInfo, time: number) {
+    info.lastNotified = time;
+    const snapshot = snapshotProgress(info);
+    info.observer(snapshot);
+}
+
+class ObservableRuntimeContext implements RuntimeContext {
+    isExecuting = true;
+    lastUpdatedTime = 0;
+
+    node: Progress.Node;
+    info: ProgressInfo;
+
+    // used for waiting for cancelled computation trees
+    onChildrenFinished?: () => void = void 0;
+
+    private checkAborted() {
+        if (this.info.abortToken.abortRequested) {
+            abort(this.info, this.node);
+        }
+    }
+
+    get shouldUpdate(): boolean {
+        this.checkAborted();
+        return now() - this.lastUpdatedTime > this.info.updateRateMs;
+    }
+
+    private updateProgress(update?: string | Partial<RuntimeContext.ProgressUpdate>) {
+        this.checkAborted();
+
+        if (!update) return;
+
+        const progress = this.node.progress;
+        if (typeof update === 'string') {
+            progress.message = update;
+        } else {
+            if (typeof update.canAbort !== 'undefined') progress.canAbort = update.canAbort;
+            if (typeof update.current !== 'undefined') progress.current = update.current;
+            if (typeof update.isIndeterminate !== 'undefined') progress.isIndeterminate = update.isIndeterminate;
+            if (typeof update.max !== 'undefined') progress.max = update.max;
+            if (typeof update.message !== 'undefined') progress.message = update.message;
+        }
+    }
+
+    update(progress?: string | Partial<RuntimeContext.ProgressUpdate>, dontNotify?: boolean): Promise<void> | void {
+        // The progress tracking and observer notification are separated
+        // because the computation can have a tree structure.
+        // All nodes of the tree should be regualarly updated at the specified frequency,
+        // however, the notification should only be invoked once per the whole tree.
+
+        this.lastUpdatedTime = now();
+        this.updateProgress(progress);
+
+        if (!!dontNotify || !shouldNotify(this.info, this.lastUpdatedTime)) return;
+
+        notifyObserver(this.info, this.lastUpdatedTime);
+
+        // The computation could have been aborted during the notifycation phase.
+        this.checkAborted();
+
+        return Scheduler.immediatePromise();
+    }
+
+    async runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> {
+        this.updateProgress(progress);
+
+        // Create a new child context and add it to the progress tree.
+        // When the child task finishes, remove the tree node.
+
+        const node: Progress.Node = { progress: defaultProgress(task), children: [] };
+        const children = this.node.children as Progress.Node[];
+        children.push(node);
+        const ctx = new ObservableRuntimeContext(this.info, node);
+        try {
+            return await execute(task, ctx);
+        } catch (e) {
+            if (Task.isAbort(e)) {
+                // need to catch the error here because otherwise
+                // promises for running child tasks in a tree-like computation
+                // will get orphaned and cause "uncaught error in Promise".
+                return void 0 as any;
+            }
+            throw e;
+        } finally {
+            // remove the progress node after the computation has finished.
+            const idx = children.indexOf(node);
+            if (idx >= 0) {
+                for (let i = idx, _i = children.length - 1; i < _i; i++) {
+                    children[i] = children[i + 1];
+                }
+                children.pop();
+            }
+            if (children.length === 0 && this.onChildrenFinished) this.onChildrenFinished();
+        }
+    }
+
+    constructor(info: ProgressInfo, node: Progress.Node) {
+        this.node = node;
+        this.info = info;
+    }
+}
+
+export { ExecuteObservable }

+ 24 - 0
src/mol-task/execution/progress.ts

@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../task'
+
+interface Progress {
+    root: Progress.Node,
+    canAbort: boolean,
+    requestAbort: (reason?: string) => void
+}
+
+namespace Progress {
+    export interface Node {
+        readonly progress: Task.Progress,
+        readonly children: ReadonlyArray<Node>
+    }
+
+    export interface Observer { (progress: Progress): void }
+}
+
+export { Progress }

+ 36 - 0
src/mol-task/execution/runtime-context.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../task'
+
+interface RuntimeContext {
+    readonly shouldUpdate: boolean,
+
+    // Idiomatic usage:
+    // if (ctx.needsYield) await ctx.yield({ ... });
+    //
+    // Alternatively, progress can be updated without notifying (and yielding) using update(progress, true).
+    // This is useful for nested tasks.
+    update(progress?: string | Partial<RuntimeContext.ProgressUpdate>, dontNotify?: boolean): Promise<void> | void,
+
+    // Run a child task that adds a new node to the progress tree.
+    // Allow to pass the progress so that the progress tree can be kept in a "good state" without having to separately call update.
+    runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T>
+}
+
+namespace RuntimeContext {
+    export interface AbortToken { isAborted: boolean }
+
+    export interface ProgressUpdate {
+        message: string,
+        isIndeterminate: boolean,
+        current: number,
+        max: number,
+        canAbort: boolean
+    }
+}
+
+export { RuntimeContext }

+ 22 - 0
src/mol-task/execution/synchronous.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../task'
+import { RuntimeContext } from './runtime-context'
+
+class SynchronousRuntimeContext implements RuntimeContext {
+    shouldUpdate: boolean = false;
+    update(progress: string | Partial<RuntimeContext.ProgressUpdate>, dontNotify?: boolean): Promise<void> | void { }
+    runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> { return ExecuteSynchronous(task); }
+}
+
+const SyncRuntimeInstance = new SynchronousRuntimeContext();
+
+function ExecuteSynchronous<T>(task: Task<T>) {
+    return task.__f(SyncRuntimeInstance);
+}
+
+export { ExecuteSynchronous }

+ 24 - 0
src/mol-task/index.ts

@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from './task'
+import { RuntimeContext } from './execution/runtime-context'
+import { ExecuteSynchronous } from './execution/synchronous'
+import { ExecuteObservable } from './execution/observable'
+import { Progress } from './execution/progress'
+import { now } from './util/now'
+import { Scheduler } from './util/scheduler'
+
+// Run the task without the ability to observe its progress.
+function Run<T>(task: Task<T>): Promise<T>;
+// Run the task with the ability to observe its progress and request cancellation.
+function Run<T>(task: Task<T>, observer: Progress.Observer, updateRateMs?: number): Promise<T>;
+function Run<T>(task: Task<T>, observer?: Progress.Observer, updateRateMs?: number): Promise<T> {
+    if (observer) return ExecuteObservable(task, observer, updateRateMs || 250);
+    return ExecuteSynchronous(task);
+}
+
+export { Task, RuntimeContext, Progress, Run, now, Scheduler }

+ 51 - 0
src/mol-task/task.ts

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { RuntimeContext } from './execution/runtime-context'
+
+// A "named function wrapper" with built in "computation tree progress tracking".
+// Use Run(t, ?observer, ?updateRate) to execute
+interface Task<T> {
+    readonly id: number,
+    readonly name: string,
+    // Do not call this directly, use Run.
+    readonly __f: (ctx: RuntimeContext) => Promise<T>,
+    // Do not call this directly, use Run.
+    readonly __onAbort: (() => void) | undefined
+}
+
+namespace Task {
+    export interface Aborted { isAborted: true, reason: string }
+    export function isAbort(e: any): e is Aborted { return !!e && !!e.isAborted; }
+    export function Aborted(reason: string): Aborted { return { isAborted: true, reason }; }
+
+    export function create<T>(name: string, f: (ctx: RuntimeContext) => Promise<T>, onAbort?: () => void): Task<T> {
+        return { id: nextId(), name, __f: f, __onAbort: onAbort };
+    }
+
+    export function constant<T>(name: string, value: T): Task<T> { return create(name, async ctx => value); }
+    export function fail(name: string, reason: string): Task<any> { return create(name, async ctx => { throw new Error(reason); }); }
+
+    export interface Progress {
+        taskId: number,
+        taskName: string,
+        startedTime: number,
+        message: string,
+        canAbort: boolean,
+        isIndeterminate: boolean,
+        current: number,
+        max: number
+    }
+
+    let _id = 0;
+    function nextId() {
+        const ret = _id;
+        _id = (_id + 1) % 0x3fffffff;
+        return ret;
+    }
+}
+
+export { Task }

+ 30 - 5
src/mol-task/util.ts

@@ -1,9 +1,34 @@
 
+// enum TaskState {
+//     Pending,
+//     Running,
+//     Aborted,
+//     Completed,
+//     Failed
+// }
 
+interface TaskState {
+
+}
+
+namespace TaskState {
+    export interface Pending { kind: 'Pending' }
+    export interface Running { kind: 'Running',  }
+
+    export interface Progress {
+        message: string,
+        isIndeterminate: boolean,
+        current: number,
+        max: number,
+        elapsedMs: number
+    }
+}
 
 type ExecutionContext = {
     run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
-    subscribe(o: (p: string, compId: number) => void): void
+    subscribe(o: (p: string, compId: number) => void): void,
+
+    requestAbort(compId: number): void
 }
 
 namespace ExecutionContext {
@@ -17,12 +42,13 @@ namespace ExecutionContext {
     export const Sync: ExecutionContext = 0 as any;
 }
 
-interface RuntimeContext extends ExecutionContext {
+interface RuntimeContext {
+    run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
     yield(name: string): Promise<void> | void
 }
 
 // if no context is specified, use the synchronous one.
-type Computation<T> = { (ctx?: RuntimeContext): Promise<T>, _id: number }
+interface Computation<T> { (ctx: RuntimeContext): Promise<T>, _id: number }
 
 function create<T>(c: (ctx: RuntimeContext) => Promise<T>): Computation<T> { return 0 as any; }
 function constant<T>(c: T) { return create(async ctx => c); }
@@ -33,7 +59,6 @@ function MultistepComputation<P, T>(name: string, steps: string[], f: MultistepF
     return params => create(async ctx => f(params, n => ctx.yield(steps[n]), ctx));
 }
 
-
 // if total count is specified, could automatically provide percentage
 type UniformlyChunkedFn<S> = (chunkSize: number, state: S, totalCount?: number) => number
 type UniformlyChunkedProvider<S> = (ctx: RuntimeContext, state: S) => Promise<S>
@@ -58,7 +83,7 @@ function readLines(str: string): Computation<string[]> {
 
 const prependHiToLines = MultistepComputation('Hi prepend', ['Parse input', 'Prepend Hi'], async (p: string, step, ctx) => {
     await step(0);
-    const lines = await ctx.run(readLines(p));
+    const lines = await readLines(p)(ctx);
     await step(1);
     const ret = lines.map(l => 'Hi ' + l);
     return ret;

+ 0 - 0
src/mol-task/context.ts → src/mol-task/util/chunked.ts


+ 0 - 0
src/mol-task/util/multistep.ts


+ 3 - 1
src/mol-task/time.ts → src/mol-task/util/now.ts

@@ -16,9 +16,11 @@ const now: () => number = (function () {
             const t = process.hrtime();
             return t[0] * 1000 + t[1] / 1000000;
         };
+    } else if (Date.now) {
+        return () => Date.now();
     } else {
         return () => +new Date();
     }
 }());
 
-export default now;
+export { now }

+ 25 - 29
src/mol-task/scheduler.ts → src/mol-task/util/scheduler.ts

@@ -10,14 +10,21 @@
  * MIT license.
  */
 
+declare var WorkerGlobalScope: any;
 function createImmediateActions() {
+    const global: any = (function () {
+        const _window = typeof window !== 'undefined' && window;
+        const _self = typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope && self;
+        const _global = typeof global !== 'undefined' && global;
+        return _window || _global || _self;
+    })();
+
     type Callback = (...args: any[]) => void;
     type Task = { callback: Callback, args: any[] }
 
     const tasksByHandle: { [handle: number]: Task } = { };
     const doc = typeof document !== 'undefined' ? document : void 0;
 
-    let currentlyRunningATask = false;
     let nextHandle = 1; // Spec says greater than zero
     let registerImmediate: ((handle: number) => void);
 
@@ -60,24 +67,9 @@ function createImmediateActions() {
     }
 
     function runIfPresent(handle: number) {
-        // From the spec: 'Wait until any invocations of this algorithm started before this one have completed.'
-        // So if we're currently running a task, we'll need to delay this invocation.
-        if (currentlyRunningATask) {
-            // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
-            // 'too much recursion' error.
-            setTimeout(runIfPresent, 0, handle);
-        } else {
-            const task = tasksByHandle[handle];
-            if (task) {
-                currentlyRunningATask = true;
-                try {
-                    run(task);
-                } finally {
-                    clearImmediate(handle);
-                    currentlyRunningATask = false;
-                }
-            }
-        }
+        const task = tasksByHandle[handle];
+        clearImmediate(handle);
+        run(task);
     }
 
     function installNextTickImplementation() {
@@ -87,9 +79,6 @@ function createImmediateActions() {
     }
 
     function canUsePostMessage() {
-        // The test against `importScripts` prevents this implementation from being installed inside a web worker,
-        // where `global.postMessage` means something completely different and can't be used for this purpose.
-        const global = typeof window !== 'undefined' ? window as any : void 0;
         if (global && global.postMessage && !global.importScripts) {
             let postMessageIsAsynchronous = true;
             const oldOnMessage = global.onmessage;
@@ -108,7 +97,6 @@ function createImmediateActions() {
         // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
 
         const messagePrefix = 'setImmediate$' + Math.random() + '$';
-        const global = typeof window !== 'undefined' ? window as any : void 0;
         const onGlobalMessage = function(event: any) {
             if (event.source === global &&
                 typeof event.data === 'string' &&
@@ -189,8 +177,13 @@ function createImmediateActions() {
 const immediateActions = (function () {
     if (typeof setImmediate !== 'undefined') {
         if (typeof window !== 'undefined') {
-            return { setImmediate: (handler: any, ...args: any[]) => window.setImmediate(handler, ...args as any), clearImmediate: (handle: any) => window.clearImmediate(handle) };
-        } else return { setImmediate, clearImmediate }
+            return {
+                setImmediate: (handler: any, ...args: any[]) => window.setImmediate(handler, ...args as any),
+                clearImmediate: (handle: any) => window.clearImmediate(handle)
+            };
+        } else {
+            return { setImmediate, clearImmediate }
+        }
     }
     return createImmediateActions();
 }());
@@ -199,9 +192,12 @@ function resolveImmediate(res: () => void) {
     immediateActions.setImmediate(res);
 }
 
-export default {
-    immediate: immediateActions.setImmediate,
+const Scheduler = {
+    setImmediate: immediateActions.setImmediate,
     clearImmediate: immediateActions.clearImmediate,
+    immediatePromise() { return new Promise<void>(resolveImmediate); },
+
+    delay<T>(timeout: number, value: T | undefined = void 0): Promise<T> { return new Promise(r => setTimeout(r, timeout, value)) }
+}
 
-    immediatePromise() { return new Promise<void>(resolveImmediate); }
-};
+export { Scheduler }

+ 129 - 0
src/perf-tests/tasks.ts

@@ -0,0 +1,129 @@
+import * as B from 'benchmark'
+import { now } from 'mol-task/util/now'
+import { Scheduler } from 'mol-task/util/scheduler'
+
+export namespace Tasks {
+    export class Yielding {
+        lastUpdated = 0;
+        yield(): Promise<void> | void {
+            const t = now();
+            if (t - this.lastUpdated < 250) return;
+            this.lastUpdated = t;
+            return Scheduler.immediatePromise();
+        }
+    }
+
+    export class CheckYielding {
+        lastUpdated = 0;
+
+        get needsYield() {
+            return now() - this.lastUpdated > 250;
+        }
+
+        yield(): Promise<void> {
+            this.lastUpdated = now();
+            return Scheduler.immediatePromise();
+        }
+    }
+
+    export async function yielding() {
+        console.time('yielding');
+        const y = new Yielding();
+        let ret = 0;
+        for (let i = 0; i < 1000000; i++) {
+            ret += +(i.toString() + i.toString());
+            if (i % 10000 === 0) await y.yield();
+        }
+        console.timeEnd('yielding');
+        console.log(ret);
+        return ret;
+    }
+
+    export async function yielding1() {
+        console.time('yielding1');
+        const y = new Yielding();
+        let ret = 0;
+        let yy: any;
+        for (let i = 0; i < 1000000; i++) {
+            ret += +(i.toString() + i.toString());
+            if (i % 10000 === 0 && (yy = y.yield())) await yy;
+        }
+        console.timeEnd('yielding1');
+        console.log(ret);
+        return ret;
+    }
+
+    export async function testYielding() {
+        console.time('check yielding');
+        const y = new CheckYielding();
+        let ret = 0;
+        for (let i = 0; i < 1000000; i++) {
+            ret += +(i.toString() + i.toString());
+            if (i % 10000 === 0 && y.needsYield) await y.yield();
+        }
+        console.timeEnd('check yielding');
+        console.log(ret);
+        return ret;
+    }
+
+    export async function baseline() {
+        console.time('baseline');
+        let ret = 0;
+        for (let i = 0; i < 1000000; i++) {
+            ret += +(i.toString() + i.toString());
+        }
+        console.timeEnd('baseline');
+        console.log(ret);
+        return ret;
+    }
+
+    export async function testImmediate() {
+        console.time('immediate');
+        let ret = 0;
+        const y = new CheckYielding();
+        for (let i = 0; i < 1000000; i++) {
+            //ret += +(i.toString() + i.toString());
+            if (i % 10000 === 0) await y.yield();
+        }
+        console.timeEnd('immediate');
+        console.log(ret);
+        return ret;
+    }
+
+    export function run() {
+        const suite = new B.Suite();
+        suite
+            .add(`yielding`, async () => { return await yielding() })
+            //.add(`test yielding`, () => testYielding().then(() => { }))
+            .on('cycle', (e: any) => console.log(String(e.target)))
+            .run();
+    }
+}
+
+(async function() {
+    // await Tasks.testImmediate();
+    // await Tasks.testImmediate();
+
+    await Tasks.baseline();
+    await Tasks.yielding();
+    await Tasks.yielding1();
+    await Tasks.testYielding();
+    await Tasks.baseline();
+    await Tasks.yielding();
+    await Tasks.yielding1();
+    await Tasks.testYielding();
+}())
+
+// console.time('test')
+// Tasks.yielding();
+// console.timeEnd('test')
+// console.time('test')
+// Tasks.yielding();
+// console.timeEnd('test')
+
+// console.time('test')
+// Tasks.testYielding();
+// console.timeEnd('test')
+// console.time('test')
+// Tasks.testYielding();
+// console.timeEnd('test')

+ 1 - 0
tsconfig.json

@@ -13,6 +13,7 @@
         "outDir": "build/node_modules",
         "baseUrl": "src",
         "paths": {
+            "mol-task": ["./mol-task", "./mol-task/index.ts"],
             "mol-comp": ["./mol-comp", "./mol-comp/index.ts"],
             "mol-util": ["./mol-util", "./mol-util/index.ts"],
             "mol-data": ["./mol-data", "./mol-data/index.ts"],